Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add UI for SAML attribute mapping #4184

Merged
merged 8 commits into from
Jun 24, 2024
124 changes: 124 additions & 0 deletions frontend/common/services/useSamlAttributeMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const samlAttributeMappingService = service
.enhanceEndpoints({ addTagTypes: ['SamlAttributeMapping'] })
.injectEndpoints({
endpoints: (builder) => ({
createSamlAttributeMapping: builder.mutation<
Res['samlAttributeMapping'],
Req['createSamlAttributeMapping']
>({
invalidatesTags: [{ id: 'LIST', type: 'SamlAttributeMapping' }],
query: (query: Req['createSamlAttributeMapping']) => ({
body: query.body,
method: 'POST',
url: `auth/saml/attribute-mapping/`,
}),
}),
deleteSamlAttributeMapping: builder.mutation<
Res['samlAttributeMapping'],
Req['deleteSamlAttributeMapping']
>({
invalidatesTags: [{ id: 'LIST', type: 'SamlAttributeMapping' }],
query: (query: Req['deleteSamlAttributeMapping']) => ({
method: 'DELETE',
url: `auth/saml/attribute-mapping/${query.attribute_id}`,
}),
}),
getSamlAttributeMapping: builder.query<
Res['samlAttributeMapping'],
Req['getSamlAttributeMapping']
>({
providesTags: () => [{ id: 'LIST', type: 'SamlAttributeMapping' }],
query: (query: Req['getSamlAttributeMapping']) => ({
url: `auth/saml/attribute-mapping/?saml_configuration=${query.saml_configuration_id}`,
}),
}),
updateSamlAttributeMapping: builder.mutation<
Res['samlAttributeMapping'],
Req['updateSamlAttributeMapping']
>({
invalidatesTags: () => [{ id: 'LIST', type: 'SamlAttributeMapping' }],
query: (query: Req['updateSamlAttributeMapping']) => ({
body: query.body,
method: 'PUT',
url: `auth/saml/attribute-mapping/${query.attribute_id}`,
}),
}),
// END OF ENDPOINTS
}),
})

export async function createSamlAttributeMapping(
store: any,
data: Req['createSamlAttributeMapping'],
options?: Parameters<
typeof samlAttributeMappingService.endpoints.createSamlAttributeMapping.initiate
>[1],
) {
return store.dispatch(
samlAttributeMappingService.endpoints.createSamlAttributeMapping.initiate(
data,
options,
),
)
}
export async function deleteSamlAttributeMapping(
store: any,
data: Req['deleteSamlAttributeMapping'],
options?: Parameters<
typeof samlAttributeMappingService.endpoints.deleteSamlAttributeMapping.initiate
>[1],
) {
return store.dispatch(
samlAttributeMappingService.endpoints.deleteSamlAttributeMapping.initiate(
data,
options,
),
)
}
export async function getSamlAttributeMapping(
store: any,
data: Req['getSamlAttributeMapping'],
options?: Parameters<
typeof samlAttributeMappingService.endpoints.getSamlAttributeMapping.initiate
>[1],
) {
return store.dispatch(
samlAttributeMappingService.endpoints.getSamlAttributeMapping.initiate(
data,
options,
),
)
}
export async function updateSamlAttributeMapping(
store: any,
data: Req['updateSamlAttributeMapping'],
options?: Parameters<
typeof samlAttributeMappingService.endpoints.updateSamlAttributeMapping.initiate
>[1],
) {
return store.dispatch(
samlAttributeMappingService.endpoints.updateSamlAttributeMapping.initiate(
data,
options,
),
)
}
// END OF FUNCTION_EXPORTS

export const {
useCreateSamlAttributeMappingMutation,
useDeleteSamlAttributeMappingMutation,
useGetSamlAttributeMappingQuery,
useUpdateSamlAttributeMappingMutation,
// END OF EXPORTS
} = samlAttributeMappingService

/* Usage examples:
const { data, isLoading } = useGetSamlAttributeMappingQuery({ id: 2 }, {}) //get hook
const [createSamlAttributeMapping, { isLoading, data, isSuccess }] = useCreateSamlAttributeMappingMutation() //create hook
samlAttributeMappingService.endpoints.getSamlAttributeMapping.select({id: 2})(store.getState()) //access data from any function
*/
18 changes: 18 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ProjectFlag,
Environment,
UserGroup,
AttributeName,
} from './responses'

export type PagedRequest<T> = T & {
Expand Down Expand Up @@ -486,5 +487,22 @@ export type Req = {
updateSamlConfiguration: { name: string; body: SAMLConfiguration }
deleteSamlConfiguration: { name: string }
createSamlConfiguration: SAMLConfiguration
getSamlAttributeMapping: { saml_configuration_id: number }
updateSamlAttributeMapping: {
attribute_id: number
body: {
saml_configuration: number
django_attribute_name: AttributeName
idp_attribute_name: string
}
}
deleteSamlAttributeMapping: { attribute_id: number }
createSamlAttributeMapping: {
body: {
saml_configuration: number
django_attribute_name: AttributeName
idp_attribute_name: string
}
}
// END OF TYPES
}
11 changes: 11 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ export type AuthType = 'EMAIL' | 'GITHUB' | 'GOOGLE'

export type SignupType = 'NO_INVITE' | 'INVITE_EMAIL' | 'INVITE_LINK'

export type AttributeName = 'email' | 'first_name' | 'last_name' | 'groups'

export type Invite = {
id: number
email: string
Expand Down Expand Up @@ -554,13 +556,21 @@ export type MetadataModelField = {
}

export type SAMLConfiguration = {
id: number
organisation: number
name: string
frontend_url: string
idp_metadata_xml?: string
allow_idp_initiated?: boolean
}

export type SAMLAttributeMapping = {
id: number
saml_configuration: number
django_attribute_name: AttributeName
idp_attribute_name: string
}

export type Res = {
segments: PagedResponse<Segment>
segment: Segment
Expand Down Expand Up @@ -679,5 +689,6 @@ export type Res = {
response_url: string
metadata_xml: string
}
samlAttributeMapping: PagedResponse<SAMLAttributeMapping>
// END OF TYPES
}
135 changes: 135 additions & 0 deletions frontend/web/components/SAMLAttributeMappingTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { FC } from 'react'

import {
useDeleteSamlAttributeMappingMutation,
useGetSamlAttributeMappingQuery,
} from 'common/services/useSamlAttributeMapping'
import PanelSearch from './PanelSearch'
import Button from './base/forms/Button'
import Icon from './Icon'
import { SAMLAttributeMapping } from 'common/types/responses'
import Format from 'common/utils/format'
import Tooltip from './Tooltip'

type SAMLAttributeMappingTableType = {
samlConfigurationId: number
}
const SAMLAttributeMappingTable: FC<SAMLAttributeMappingTableType> = ({
samlConfigurationId,
}) => {
const { data } = useGetSamlAttributeMappingQuery(
{
saml_configuration_id: samlConfigurationId,
},
{ skip: !samlConfigurationId },
)

const [deleteSamlAttribute] = useDeleteSamlAttributeMappingMutation()

return (
<div>
<PanelSearch
className='no-pad overflow-visible mt-4'
id='features-list'
renderSearchWithNoResults
itemHeight={65}
isLoading={false}
header={
<Row className='table-header'>
<Flex className='table-column px-3'>
<div className='font-weight-medium'>SAML Attribute Name</div>
</Flex>
<Flex className='table-column px-3'>
<div className='table-column' style={{ width: '375px' }}>
IDP Attribute Name
</div>
</Flex>
</Row>
}
items={data?.results || []}
renderRow={(attribute: SAMLAttributeMapping) => (
<Row
space
className='list-item'
key={attribute.django_attribute_name}
>
<Flex className='table-column px-3'>
<div className='font-weight-medium mb-1'>
{Format.camelCase(
attribute.django_attribute_name.replace(/_/g, ' '),
)}
</div>
</Flex>
<Flex className='table-column px-3'>
<Tooltip
title={
<div
className='table-column'
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '305px',
}}
>
{attribute.idp_attribute_name}
</div>
}
>
{attribute.idp_attribute_name}
</Tooltip>
</Flex>
<div className='table-column'>
<Button
id='delete-attribute'
data-test='delete-attribute'
type='button'
onClick={(e) => {
openModal2(
'Delete SAML attribute',
<div>
<div>
Are you sure you want to delete the attribute{' '}
<b>{`${Format.camelCase(
attribute.django_attribute_name.replace(/_/g, ' '),
)}?`}</b>
</div>
<div className='text-right'>
<Button
className='mr-2'
onClick={() => {
closeModal2()
}}
>
Cancel
</Button>
<Button
theme='danger'
onClick={() => {
deleteSamlAttribute({
attribute_id: attribute.id,
}).then(() => {
toast('SAML attribute deleted')
closeModal2()
})
}}
>
Delete
</Button>
</div>
</div>,
)
e.stopPropagation()
e.preventDefault()
}}
className='btn btn-with-icon'
>
<Icon name='trash-2' width={20} fill='#656D7B' />
</Button>
</div>
</Row>
)}
/>
</div>
)
}
export default SAMLAttributeMappingTable
4 changes: 2 additions & 2 deletions frontend/web/components/SamlTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
type='button'
onClick={(e) => {
openModal(
'Delete Github Integration',
'Delete SAML configuration',
<div>
<div>
Are you sure you want to delete the SAML
Expand All @@ -105,7 +105,7 @@ const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
<Button
className='mr-2'
onClick={() => {
closeModal2()
closeModal()
}}
>
Cancel
Expand Down
5 changes: 4 additions & 1 deletion frontend/web/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ const Tooltip: FC<TooltipProps> = ({
{plainText ? (
`${children}`
) : (
<div dangerouslySetInnerHTML={{ __html: children }} />
<div
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: children }}
/>
)}
</ReactTooltip>
)}
Expand Down
Loading
Loading