Skip to content

Commit 4e5fa9f

Browse files
refactor: modernise Server-side SDK Keys page (#7003)
1 parent 6bf055c commit 4e5fa9f

File tree

12 files changed

+434
-1
lines changed

12 files changed

+434
-1
lines changed

frontend/common/services/useServersideEnvironmentKey.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export const serversideEnvironmentKeyService = service
3232
Res['serversideEnvironmentKeys'],
3333
Req['getServersideEnvironmentKeys']
3434
>({
35+
// TODO(#7140): remove res?.id tag when legacy ServerSideSDKKeys.js is deleted
3536
providesTags: (res) => [
37+
{ id: 'LIST', type: 'ServersideEnvironmentKey' },
3638
{ id: res?.id, type: 'ServersideEnvironmentKey' },
3739
],
3840
query: (query: Req['getServersideEnvironmentKeys']) => ({
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { test, expect } from '../test-setup'
2+
import { byId, log, createHelpers } from '../helpers'
3+
import { E2E_USER, PASSWORD, E2E_TEST_PROJECT } from '../config'
4+
5+
test.describe('SDK Keys Tests', () => {
6+
test('Server-side SDK keys can be created and deleted @oss', async ({
7+
page,
8+
}) => {
9+
const {
10+
click,
11+
gotoProject,
12+
login,
13+
setText,
14+
waitForElementVisible,
15+
} = createHelpers(page)
16+
17+
log('Login')
18+
await login(E2E_USER, PASSWORD)
19+
await gotoProject(E2E_TEST_PROJECT)
20+
21+
log('Navigate to SDK Keys')
22+
await click('#sdk-keys-link')
23+
await waitForElementVisible('#server-side-keys-list, button:has-text("Create Server-side Environment Key")')
24+
25+
log('Create server-side key')
26+
await page.locator('button').filter({ hasText: 'Create Server-side Environment Key' }).click()
27+
await waitForElementVisible('.modal-body')
28+
await setText('[name="name"]', 'Test E2E Key')
29+
await page.getByRole('button', { name: 'Create', exact: true }).click()
30+
31+
log('Verify key appears in list')
32+
await page.waitForSelector('#server-side-keys-list')
33+
const keyRow = page.locator('.list-item').filter({ hasText: 'Test E2E Key' })
34+
await keyRow.waitFor({ state: 'visible', timeout: 10000 })
35+
36+
log('Delete server-side key')
37+
await keyRow.locator('#remove-sdk-key').click()
38+
await waitForElementVisible('#confirm-btn-yes')
39+
await click('#confirm-btn-yes')
40+
41+
log('Verify key is removed')
42+
await expect(keyRow).toHaveCount(0, { timeout: 10000 })
43+
})
44+
})
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { FC } from 'react'
2+
import Button from 'components/base/forms/Button'
3+
import Input from 'components/base/forms/Input'
4+
import Icon from 'components/Icon'
5+
import PageTitle from 'components/PageTitle'
6+
import Utils from 'common/utils/utils'
7+
import { useRouteMatch } from 'react-router-dom'
8+
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
9+
import { ServerSideSDKKeys } from './components'
10+
11+
interface RouteParams {
12+
environmentId: string
13+
projectId: string
14+
}
15+
16+
const SDKKeysPage: FC = () => {
17+
const match = useRouteMatch<RouteParams>()
18+
const environmentId = match?.params?.environmentId
19+
const projectId = match?.params?.projectId
20+
21+
const { data: environments } = useGetEnvironmentsQuery(
22+
{ projectId: parseInt(projectId, 10) },
23+
{ skip: !projectId },
24+
)
25+
26+
const environmentName =
27+
environments?.results?.find((env) => env.api_key === environmentId)?.name ??
28+
''
29+
30+
const handleCopy = () => Utils.copyToClipboard(environmentId)
31+
32+
return (
33+
<div
34+
data-test='sdk-keys-page'
35+
id='sdk-keys-page'
36+
className='app-container container'
37+
>
38+
<PageTitle title='Client-side Environment Key'>
39+
Use this key to initialise{' '}
40+
<Button
41+
theme='text'
42+
href='https://docs.flagsmith.com/clients/overview#client-side-sdks'
43+
target='_blank'
44+
>
45+
Client-side
46+
</Button>{' '}
47+
SDKs.
48+
</PageTitle>
49+
<div className='col-md-6'>
50+
<Row>
51+
<Flex>
52+
<Input
53+
value={environmentId}
54+
inputClassName='input input--wide'
55+
type='text'
56+
title={<h3>Client-side Environment Key</h3>}
57+
placeholder='Client-side Environment Key'
58+
/>
59+
</Flex>
60+
<Button onClick={handleCopy} className='ml-2 btn-with-icon text-body'>
61+
<Icon name='copy' width={20} />
62+
</Button>
63+
</Row>
64+
</div>
65+
<hr className='py-0 my-4' />
66+
<ServerSideSDKKeys
67+
environmentId={environmentId}
68+
environmentName={environmentName}
69+
/>
70+
</div>
71+
)
72+
}
73+
74+
export default SDKKeysPage
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { FC } from 'react'
2+
import Utils from 'common/utils/utils'
3+
import SDKKeysPageLegacy from 'components/SDKKeysPage'
4+
import SDKEnvironmentKeysSettings from './SDKEnvironmentKeysSettings'
5+
6+
const SDKKeysPage: FC = () => {
7+
if (Utils.getFlagsmithHasFeature('rtk_server_side_sdk_keys')) {
8+
return <SDKEnvironmentKeysSettings />
9+
}
10+
return <SDKKeysPageLegacy />
11+
}
12+
13+
export default SDKKeysPage
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { FC, useActionState, useEffect, useRef } from 'react'
2+
import Button from 'components/base/forms/Button'
3+
import ModalHR from 'components/modals/ModalHR'
4+
5+
type CreateServerSideKeyModalProps = {
6+
environmentName: string
7+
onSubmit: (name: string) => Promise<void>
8+
}
9+
10+
const CreateServerSideKeyModal: FC<CreateServerSideKeyModalProps> = ({
11+
environmentName,
12+
onSubmit,
13+
}) => {
14+
const inputRef = useRef<HTMLInputElement>(null)
15+
16+
useEffect(() => {
17+
const timer = setTimeout(() => inputRef.current?.focus(), 500)
18+
return () => clearTimeout(timer)
19+
}, [])
20+
21+
const [error, submitAction, isPending] = useActionState(
22+
async (_prev: string | null, formData: FormData) => {
23+
const name = (formData.get('name') as string)?.trim()
24+
if (!name) return 'Name is required'
25+
try {
26+
await onSubmit(name)
27+
return null
28+
} catch {
29+
return 'Failed to create key. Please try again.'
30+
}
31+
},
32+
null,
33+
)
34+
35+
return (
36+
<div>
37+
<form action={submitAction}>
38+
<div className='modal-body'>
39+
<div className='mb-2'>
40+
This will create a Server-side Environment Key for the environment{' '}
41+
<strong>{environmentName}</strong>.
42+
</div>
43+
<InputGroup
44+
title='Key Name'
45+
placeholder='New Key'
46+
className='mb-2'
47+
ref={inputRef}
48+
inputProps={{
49+
className: 'full-width modal-input',
50+
name: 'name',
51+
}}
52+
/>
53+
{error && <div className='text-danger mt-2'>{error}</div>}
54+
</div>
55+
<ModalHR />
56+
<div className='modal-footer'>
57+
<Button onClick={closeModal} theme='secondary' className='mr-2'>
58+
Cancel
59+
</Button>
60+
<Button type='submit' disabled={isPending}>
61+
{isPending ? 'Creating...' : 'Create'}
62+
</Button>
63+
</div>
64+
</form>
65+
</div>
66+
)
67+
}
68+
69+
export default CreateServerSideKeyModal
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { FC } from 'react'
2+
import cn from 'classnames'
3+
import Button from 'components/base/forms/Button'
4+
import Icon from 'components/Icon'
5+
import Token from 'components/Token'
6+
import Utils from 'common/utils/utils'
7+
8+
type ServerSideKeyRowProps = {
9+
id: string
10+
keyValue: string
11+
name: string
12+
isDeleting: boolean
13+
onRemove: (id: string, name: string) => void
14+
}
15+
16+
const ServerSideKeyRow: FC<ServerSideKeyRowProps> = ({
17+
id,
18+
isDeleting,
19+
keyValue,
20+
name,
21+
onRemove,
22+
}) => {
23+
return (
24+
<Row
25+
className={cn('list-item', {
26+
'opacity-50 pointer-events-none': isDeleting,
27+
})}
28+
>
29+
<Flex className='table-column px-3 font-weight-medium'>{name}</Flex>
30+
<div className='table-column'>
31+
<Token style={{ width: 280 }} token={keyValue} />
32+
</div>
33+
<Button
34+
onClick={() => {
35+
Utils.copyToClipboard(keyValue)
36+
}}
37+
className='ml-2 btn-with-icon text-body'
38+
>
39+
<Icon name='copy' width={20} />
40+
</Button>
41+
<div className='table-column'>
42+
<Button
43+
onClick={() => onRemove(id, name)}
44+
id='remove-sdk-key'
45+
className='btn btn-with-icon text-body'
46+
>
47+
<Icon name='trash-2' width={20} />
48+
</Button>
49+
</div>
50+
</Row>
51+
)
52+
}
53+
54+
export default ServerSideKeyRow
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { FC } from 'react'
2+
import Button from 'components/base/forms/Button'
3+
import Tooltip from 'components/Tooltip'
4+
import Constants from 'common/constants'
5+
import { useHasPermission } from 'common/providers/Permission'
6+
import { EnvironmentPermission } from 'common/types/permissions.types'
7+
import CreateServerSideKeyModal from './CreateServerSideKeyModal'
8+
import ServerSideKeyRow from './ServerSideKeyRow'
9+
import { useServerSideKeys } from 'components/pages/sdk-keys/hooks/useServerSideKeys'
10+
11+
type ServerSideKey = {
12+
id: string
13+
key: string
14+
name: string
15+
}
16+
17+
type ServerSideSDKKeysProps = {
18+
environmentId: string
19+
environmentName: string
20+
}
21+
22+
const ServerSideSDKKeys: FC<ServerSideSDKKeysProps> = ({
23+
environmentId,
24+
environmentName,
25+
}) => {
26+
const { permission: isAdmin } = useHasPermission({
27+
id: environmentId,
28+
level: 'environment',
29+
permission: EnvironmentPermission.ADMIN,
30+
})
31+
32+
const { handleCreateKey, handleRemove, isDeletingKey, isLoading, keys } =
33+
useServerSideKeys({ environmentId })
34+
35+
const handleCreate = () => {
36+
openModal(
37+
'Create Server-side Environment Keys',
38+
<CreateServerSideKeyModal
39+
environmentName={environmentName}
40+
onSubmit={handleCreateKey}
41+
/>,
42+
'p-0',
43+
)
44+
}
45+
46+
const filterByName = (item: ServerSideKey, search: string) =>
47+
item.name.toLowerCase().includes(search.toLowerCase())
48+
49+
const renderKeyRow = ({ id, key, name }: ServerSideKey) => (
50+
<ServerSideKeyRow
51+
id={id}
52+
keyValue={key}
53+
name={name}
54+
isDeleting={isDeletingKey(id)}
55+
onRemove={handleRemove}
56+
/>
57+
)
58+
59+
return (
60+
<FormGroup className='my-4'>
61+
<div className='col-md-6'>
62+
<h5 className='mb-2'>Server-side Environment Keys</h5>
63+
<p className='fs-small lh-sm mb-0'>
64+
Flags can be evaluated locally within your own Server environments
65+
using our{' '}
66+
<Button
67+
theme='text'
68+
href='https://docs.flagsmith.com/clients/overview#server-side-sdks'
69+
target='_blank'
70+
>
71+
Server-side Environment Keys
72+
</Button>
73+
.
74+
</p>
75+
<p className='fs-small lh-sm mb-0'>
76+
Server-side SDKs should be initialised with a Server-side Environment
77+
Key.
78+
</p>
79+
{isAdmin ? (
80+
<Button onClick={handleCreate} className='my-4'>
81+
Create Server-side Environment Key
82+
</Button>
83+
) : (
84+
<Tooltip
85+
title={
86+
<Button className='my-4' disabled>
87+
Create Server-side Environment Key
88+
</Button>
89+
}
90+
place='right'
91+
>
92+
{Constants.environmentPermissions(EnvironmentPermission.ADMIN)}
93+
</Tooltip>
94+
)}
95+
</div>
96+
{isLoading && (
97+
<div className='text-center'>
98+
<Loader />
99+
</div>
100+
)}
101+
{keys && !!keys.length && (
102+
<PanelSearch
103+
id='server-side-keys-list'
104+
title='Server-side Environment Keys'
105+
className='no-pad'
106+
items={keys}
107+
filterRow={filterByName}
108+
renderRow={renderKeyRow}
109+
/>
110+
)}
111+
</FormGroup>
112+
)
113+
}
114+
115+
export default ServerSideSDKKeys
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as CreateServerSideKeyModal } from './CreateServerSideKeyModal'
2+
export { default as ServerSideKeyRow } from './ServerSideKeyRow'
3+
export { default as ServerSideSDKKeys } from './ServerSideSDKKeys'

0 commit comments

Comments
 (0)