Skip to content
Open
4 changes: 1 addition & 3 deletions frontend/common/services/useServersideEnvironmentKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ export const serversideEnvironmentKeyService = service
Res['serversideEnvironmentKeys'],
Req['getServersideEnvironmentKeys']
>({
providesTags: (res) => [
{ id: res?.id, type: 'ServersideEnvironmentKey' },
],
providesTags: [{ id: 'LIST', type: 'ServersideEnvironmentKey' }],
query: (query: Req['getServersideEnvironmentKeys']) => ({
url: `environments/${query.environmentId}/api-keys/`,
}),
Expand Down
42 changes: 29 additions & 13 deletions frontend/web/components/SDKKeysPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { FC } from 'react'
import { FC } from 'react'
import Button from './base/forms/Button'
import Input from './base/forms/Input'
import Icon from './Icon'
import ServerSideSDKKeys from './ServerSideSDKKeys'
import ServerSideSDKKeysLegacy from './ServerSideSDKKeys'
import { ServerSideSDKKeys } from './pages/sdk-keys/components'
import PageTitle from './PageTitle'
import Utils from 'common/utils/utils'
import { useRouteMatch } from 'react-router-dom'
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'

interface RouteParams {
environmentId: string
Expand All @@ -15,19 +17,31 @@ interface RouteParams {
const SDKKeysPage: FC = () => {
const match = useRouteMatch<RouteParams>()
const environmentId = match?.params?.environmentId
const projectId = match?.params?.projectId

const { data: environments } = useGetEnvironmentsQuery(
{ projectId: parseInt(projectId, 10) },
{ skip: !projectId },
)

const environmentName =
environments?.results?.find((env) => env.api_key === environmentId)?.name ??
''

const handleCopy = () => Utils.copyToClipboard(environmentId)

return (
<div
data-test='segments-page'
id='segments-page'
data-test='sdk-keys-page'
id='sdk-keys-page'
className='app-container container'
>
<PageTitle title='Client-side Environment Key'>
Use this key to initialise{' '}
<Button
theme='text'
href='https://docs.flagsmith.com/clients/overview#client-side-sdks'
target='__blank'
target='_blank'
>
Client-side
</Button>{' '}
Expand All @@ -44,18 +58,20 @@ const SDKKeysPage: FC = () => {
placeholder='Client-side Environment Key'
/>
</Flex>
<Button
onClick={() => {
Utils.copyToClipboard(environmentId)
}}
className='ml-2 btn-with-icon'
>
<Icon name='copy' width={20} fill='#656D7B' />
<Button onClick={handleCopy} className='ml-2 btn-with-icon text-body'>
<Icon name='copy' width={20} />
</Button>
</Row>
</div>
<hr className='py-0 my-4' />
<ServerSideSDKKeys environmentId={environmentId} />
{Utils.getFlagsmithHasFeature('rtk_server_side_sdk_keys') ? (
<ServerSideSDKKeys
environmentId={environmentId}
environmentName={environmentName}
/>
) : (
<ServerSideSDKKeysLegacy environmentId={environmentId} />
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { FC, useActionState, useEffect, useRef } from 'react'
import Button from 'components/base/forms/Button'
import ModalHR from 'components/modals/ModalHR'

type CreateServerSideKeyModalProps = {
environmentName: string
onSubmit: (name: string) => Promise<void>
}

const CreateServerSideKeyModal: FC<CreateServerSideKeyModalProps> = ({
environmentName,
onSubmit,
}) => {
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
const timer = setTimeout(() => inputRef.current?.focus(), 500)
return () => clearTimeout(timer)
}, [])

const [error, submitAction, isPending] = useActionState(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React 19's useActionState replaces the manual useState(false) for isSaving we had before. It manages pending and error states automatically — isPending resets on both success and failure, so the button can't get stuck disabled. The action receives FormData natively from <form action={...}>, removing the need for controlled input state and Utils.preventDefault.

async (_prev: string | null, formData: FormData) => {
const name = (formData.get('name') as string)?.trim()
if (!name) return 'Name is required'
try {
await onSubmit(name)
return null
} catch {
return 'Failed to create key. Please try again.'
}
},
null,
)

return (
<div>
<form action={submitAction}>
<div className='modal-body'>
<div className='mb-2'>
This will create a Server-side Environment Key for the environment{' '}
<strong>{environmentName}</strong>.
</div>
<InputGroup
title='Key Name'
placeholder='New Key'
className='mb-2'
ref={inputRef}
inputProps={{
className: 'full-width modal-input',
name: 'name',
}}
/>
{error && <div className='text-danger mt-2'>{error}</div>}
</div>
<ModalHR />
<div className='modal-footer'>
<Button onClick={closeModal} theme='secondary' className='mr-2'>
Cancel
</Button>
<Button type='submit' disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</Button>
</div>
</form>
</div>
)
}

export default CreateServerSideKeyModal
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { FC } from 'react'
import cn from 'classnames'
import Button from 'components/base/forms/Button'
import Icon from 'components/Icon'
import Token from 'components/Token'
import Utils from 'common/utils/utils'

type ServerSideKeyRowProps = {
id: string
keyValue: string
name: string
isDeleting: boolean
onRemove: (id: string, name: string) => void
}

const ServerSideKeyRow: FC<ServerSideKeyRowProps> = ({
id,
isDeleting,
keyValue,
name,
onRemove,
}) => {
return (
<Row
className={cn('list-item', {
'opacity-50 pointer-events-none': isDeleting,
})}
>
<Flex className='table-column px-3 font-weight-medium'>{name}</Flex>
<div className='table-column'>
<Token style={{ width: 280 }} token={keyValue} />
</div>
<Button
onClick={() => {
Utils.copyToClipboard(keyValue)
}}
className='ml-2 btn-with-icon text-body'
>
<Icon name='copy' width={20} />
</Button>
<div className='table-column'>
<Button
onClick={() => onRemove(id, name)}
id='remove-sdk-key'
className='btn btn-with-icon text-body'
>
<Icon name='trash-2' width={20} />
</Button>
</div>
</Row>
)
}

export default ServerSideKeyRow
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { FC } from 'react'
import Button from 'components/base/forms/Button'
import Tooltip from 'components/Tooltip'
import Constants from 'common/constants'
import { useHasPermission } from 'common/providers/Permission'
import { EnvironmentPermission } from 'common/types/permissions.types'
import CreateServerSideKeyModal from './CreateServerSideKeyModal'
import ServerSideKeyRow from './ServerSideKeyRow'
import { useServerSideKeys } from 'components/pages/sdk-keys/hooks/useServerSideKeys'

type ServerSideKey = {
id: string
key: string
name: string
}

type ServerSideSDKKeysProps = {
environmentId: string
environmentName: string
}

const ServerSideSDKKeys: FC<ServerSideSDKKeysProps> = ({
environmentId,
environmentName,
}) => {
const { permission: isAdmin } = useHasPermission({
id: environmentId,
level: 'environment',
permission: EnvironmentPermission.ADMIN,
})

const { handleCreateKey, handleRemove, isDeletingKey, isLoading, keys } =
useServerSideKeys({ environmentId })

const handleCreate = () => {
openModal(
'Create Server-side Environment Keys',
<CreateServerSideKeyModal
environmentName={environmentName}
onSubmit={handleCreateKey}
/>,
'p-0',
)
}

const filterByName = (item: ServerSideKey, search: string) =>
item.name.toLowerCase().includes(search.toLowerCase())

const renderKeyRow = ({ id, key, name }: ServerSideKey) => (
<ServerSideKeyRow
id={id}
keyValue={key}
name={name}
isDeleting={isDeletingKey(id)}
onRemove={handleRemove}
/>
)

return (
<FormGroup className='my-4'>
<div className='col-md-6'>
<h5 className='mb-2'>Server-side Environment Keys</h5>
<p className='fs-small lh-sm mb-0'>
Flags can be evaluated locally within your own Server environments
using our{' '}
<Button
theme='text'
href='https://docs.flagsmith.com/clients/overview#server-side-sdks'
target='_blank'
>
Server-side Environment Keys
</Button>
.
</p>
<p className='fs-small lh-sm mb-0'>
Server-side SDKs should be initialised with a Server-side Environment
Key.
</p>
{isAdmin ? (
<Button onClick={handleCreate} className='my-4'>
Create Server-side Environment Key
</Button>
) : (
<Tooltip
title={
<Button className='my-4' disabled>
Create Server-side Environment Key
</Button>
}
place='right'
>
{Constants.environmentPermissions(EnvironmentPermission.ADMIN)}
</Tooltip>
)}
</div>
{isLoading && (
<div className='text-center'>
<Loader />
</div>
)}
{keys && !!keys.length && (
<PanelSearch
id='server-side-keys-list'
title='Server-side Environment Keys'
className='no-pad'
items={keys}
filterRow={filterByName}
renderRow={renderKeyRow}
/>
)}
</FormGroup>
)
}

export default ServerSideSDKKeys
3 changes: 3 additions & 0 deletions frontend/web/components/pages/sdk-keys/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as CreateServerSideKeyModal } from './CreateServerSideKeyModal'
export { default as ServerSideKeyRow } from './ServerSideKeyRow'
export { default as ServerSideSDKKeys } from './ServerSideSDKKeys'
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import {
useCreateServersideEnvironmentKeysMutation,
useDeleteServersideEnvironmentKeysMutation,
useGetServersideEnvironmentKeysQuery,
} from 'common/services/useServersideEnvironmentKey'

type UseServerSideKeysParams = {
environmentId: string
}

export const useServerSideKeys = ({
environmentId,
}: UseServerSideKeysParams) => {
const { data: keys, isLoading } = useGetServersideEnvironmentKeysQuery(
{ environmentId },
{ skip: !environmentId },
)

const [createKey] = useCreateServersideEnvironmentKeysMutation()
const [deleteKey, { isLoading: isDeleting, originalArgs: deleteArgs }] =
useDeleteServersideEnvironmentKeysMutation()

const handleCreateKey = async (name: string) => {
await createKey({ data: { name }, environmentId }).unwrap()
closeModal()
}

const handleRemove = (id: string, name: string) => {
openConfirm({
body: (
<div>
Are you sure you want to remove the SDK key <strong>{name}</strong>?
This action cannot be undone.
</div>
),
destructive: true,
onYes: () => {
deleteKey({ environmentId, id })
},
title: 'Delete Server-side Environment Keys',
yesText: 'Confirm',
})
}

const isDeletingKey = (id: string) => isDeleting && deleteArgs?.id === id

return {
handleCreateKey,
handleRemove,
isDeletingKey,
isLoading,
keys,
}
}
Loading
Loading