[go: nahoru, domu]

Skip to content

Commit

Permalink
feat: Add UI for SAML attribute mapping (Flagsmith#4184)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthew Elwell <matthew.elwell@flagsmith.com>
  • Loading branch information
novakzaballa and matthewelwell committed Jun 24, 2024
1 parent 5c25c41 commit 318fb85
Show file tree
Hide file tree
Showing 7 changed files with 448 additions and 34 deletions.
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

0 comments on commit 318fb85

Please sign in to comment.