diff --git a/sdks/js/packages/core/react/components/organization/api-keys/add.tsx b/sdks/js/packages/core/react/components/organization/api-keys/add.tsx index 26975208b..c7a40d6e9 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/add.tsx +++ b/sdks/js/packages/core/react/components/organization/api-keys/add.tsx @@ -70,7 +70,7 @@ export const AddServiceAccount = () => { } }); } - } catch ({ error }: any) { + } catch (error: any) { toast.error('Something went wrong', { description: error.message }); diff --git a/sdks/js/packages/core/react/components/organization/api-keys/columns.tsx b/sdks/js/packages/core/react/components/organization/api-keys/columns.tsx index 716fa34ab..cdf398240 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/columns.tsx +++ b/sdks/js/packages/core/react/components/organization/api-keys/columns.tsx @@ -1,7 +1,7 @@ import { TrashIcon } from '@radix-ui/react-icons'; import { ApsaraColumnDef } from '@raystack/apsara'; import { Button, Flex, Text } from '@raystack/apsara/v1'; -import { Link } from '@tanstack/react-router'; +import { Link, useNavigate } from '@tanstack/react-router'; import dayjs from 'dayjs'; import { V1Beta1ServiceUser } from '~/api-client'; @@ -53,16 +53,26 @@ export const getColumns = ({ } }, cell: ({ row, getValue }) => { - return ( - - ); + return ; } } ]; }; + +function ServiceAccountDeleteAction({ id }: { id: string }) { + const navigate = useNavigate({ from: '/api-keys' }); + + function onDeleteClick() { + return navigate({ to: '/api-keys/$id/delete', params: { id: id } }); + } + return ( + + ); +} diff --git a/sdks/js/packages/core/react/components/organization/api-keys/delete.tsx b/sdks/js/packages/core/react/components/organization/api-keys/delete.tsx new file mode 100644 index 000000000..7da9e0ede --- /dev/null +++ b/sdks/js/packages/core/react/components/organization/api-keys/delete.tsx @@ -0,0 +1,104 @@ +import { Dialog, Separator, Image } from '@raystack/apsara'; +import styles from './styles.module.css'; +import { Button, Flex, Text, toast } from '@raystack/apsara/v1'; +import cross from '~/react/assets/cross.svg'; +import { useNavigate, useParams } from '@tanstack/react-router'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { useState } from 'react'; + +export const DeleteServiceAccount = () => { + const { id } = useParams({ from: '/api-keys/$id/delete' }); + const navigate = useNavigate({ from: '/api-keys/$id/delete' }); + const { client, activeOrganization: organization } = useFrontier(); + const [isLoading, setIsLoading] = useState(false); + + const orgId = organization?.id; + + async function onDeleteClick() { + try { + setIsLoading(true); + await client?.frontierServiceDeleteServiceUser(id, { org_id: orgId }); + navigate({ + to: '/api-keys', + state: { + refetch: true + } + }); + toast.success('Service account deleted'); + } catch (err: any) { + toast.error('Unable to delete service account', { + description: err?.message + }); + } finally { + setIsLoading(false); + } + } + + function onCancel() { + navigate({ to: '/api-keys' }); + } + + return ( + + {/* @ts-ignore */} + + + + Delete Service Account + + + cross navigate({ to: '/api-keys' })} + data-test-id="frontier-sdk-delete-service-account-close-btn" + /> + + + + + + This is an irreversible and permanent action doing this might result + in deletion of the service account and the keys associated with it. + Do you wish to proceed? + + + + + + + + + + ); +}; diff --git a/sdks/js/packages/core/react/components/organization/api-keys/index.tsx b/sdks/js/packages/core/react/components/organization/api-keys/index.tsx index 147c71bfe..d4128d7fb 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/index.tsx +++ b/sdks/js/packages/core/react/components/organization/api-keys/index.tsx @@ -15,7 +15,7 @@ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import Skeleton from 'react-loading-skeleton'; import { getColumns } from './columns'; import { V1Beta1ServiceUser } from '~/api-client/dist'; -import { Outlet, useNavigate } from '@tanstack/react-router'; +import { Outlet, useLocation, useNavigate } from '@tanstack/react-router'; const NoServiceAccounts = ({ config @@ -165,6 +165,8 @@ const ServiceAccountsTable = ({ export default function ApiKeys() { const [serviceUsers, setServiceUsers] = useState([]); const [isServiceUsersLoading, setIsServiceUsersLoading] = useState(false); + const location = useLocation(); + const refetch = location?.state?.refetch; const { activeOrganization: organization, @@ -196,7 +198,7 @@ export default function ApiKeys() { if (organization?.id && canUpdateWorkspace) { getServiceAccounts(organization?.id); } - }, [organization?.id, client, canUpdateWorkspace]); + }, [organization?.id, client, canUpdateWorkspace, refetch]); const isLoading = isActiveOrganizationLoading || diff --git a/sdks/js/packages/core/react/components/organization/api-keys/service-user/delete.tsx b/sdks/js/packages/core/react/components/organization/api-keys/service-user/delete.tsx new file mode 100644 index 000000000..25dd4a8a4 --- /dev/null +++ b/sdks/js/packages/core/react/components/organization/api-keys/service-user/delete.tsx @@ -0,0 +1,114 @@ +import { Dialog, Separator, Image } from '@raystack/apsara'; +import styles from './styles.module.css'; +import { Button, Flex, Text, toast } from '@raystack/apsara/v1'; +import cross from '~/react/assets/cross.svg'; +import { useNavigate, useParams } from '@tanstack/react-router'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { useState } from 'react'; +import { DEFAULT_API_PLATFORM_APP_NAME } from '~/react/utils/constants'; + +export const DeleteServiceAccountKey = () => { + const { id, tokenId } = useParams({ + from: '/api-keys/$id/key/$tokenId/delete' + }); + const navigate = useNavigate({ from: '/api-keys/$id/key/$tokenId/delete' }); + const { client, config } = useFrontier(); + const [isLoading, setIsLoading] = useState(false); + + async function onDeleteClick() { + try { + setIsLoading(true); + await client?.frontierServiceDeleteServiceUserToken(id, tokenId); + navigate({ + to: '/api-keys/$id', + params: { + id: id + }, + state: { + refetch: true + } + }); + toast.success('Service account key revoked'); + } catch (err: any) { + toast.error('Unable to revoke service account key', { + description: err?.message + }); + } finally { + setIsLoading(false); + } + } + + function onCancel() { + navigate({ + to: '/api-keys/$id', + params: { + id: id + } + }); + } + const appName = config?.apiPlatform?.appName || DEFAULT_API_PLATFORM_APP_NAME; + + return ( + + {/* @ts-ignore */} + + + + Revoke API Key + + + cross + + + + + + This is an irreversible action doing this might lead to + discontinuation of access to the {appName} features. Do you wish to + proceed? + + + + + + + + + + ); +}; diff --git a/sdks/js/packages/core/react/components/organization/api-keys/service-user/index.tsx b/sdks/js/packages/core/react/components/organization/api-keys/service-user/index.tsx index bf2e464ee..33a61e79a 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/service-user/index.tsx +++ b/sdks/js/packages/core/react/components/organization/api-keys/service-user/index.tsx @@ -48,13 +48,25 @@ const Headings = ({ const ServiceUserTokenItem = ({ token, - isLoading + isLoading, + serviceUserId }: { token: V1Beta1ServiceUserToken; isLoading: boolean; + serviceUserId: string; }) => { const { copy } = useCopyToClipboard(); + const navigate = useNavigate({ from: '/api-keys/$id' }); + function onRevokeClick() { + navigate({ + to: '/api-keys/$id/key/$tokenId/delete', + params: { + tokenId: token?.id || '', + id: serviceUserId + } + }); + } return ( @@ -72,6 +84,7 @@ const ServiceUserTokenItem = ({ variant="secondary" size={'small'} data-test-id={`frontier-sdk-service-account-token-revoke-btn`} + onClick={onRevokeClick} > Revoke @@ -102,10 +115,12 @@ const ServiceUserTokenItem = ({ const SerivceUserTokenList = ({ isLoading, - tokens + tokens, + serviceUserId }: { isLoading: boolean; tokens: V1Beta1ServiceUserToken[]; + serviceUserId: string; }) => { const tokenList = isLoading ? [ @@ -122,6 +137,7 @@ const SerivceUserTokenList = ({ token={token} key={token?.id} isLoading={isLoading} + serviceUserId={serviceUserId} /> ))} @@ -133,9 +149,6 @@ export default function ServiceUserPage() { const { client, config } = useFrontier(); const navigate = useNavigate({ from: '/api-keys/$id' }); - const location = useLocation(); - const existingToken = location?.state?.token; - const [serviceUser, setServiceUser] = useState(); const [isServiceUserLoadning, setIsServiceUserLoading] = useState(false); @@ -146,6 +159,10 @@ export default function ServiceUserPage() { const [isServiceUserTokensLoading, setIsServiceUserTokensLoading] = useState(false); + const location = useLocation(); + const existingToken = location?.state?.token; + const refetch = location?.state?.refetch; + const getServiceUser = useCallback( async (serviceUserId: string) => { try { @@ -187,7 +204,7 @@ export default function ServiceUserPage() { getServiceUserTokens(id); } } - }, [id, getServiceUser, getServiceUserTokens, existingToken?.id]); + }, [id, getServiceUser, getServiceUserTokens, existingToken?.id, refetch]); const tokenList = existingToken ? [existingToken, ...serviceUserTokens] @@ -220,7 +237,11 @@ export default function ServiceUserPage() { config={config?.apiPlatform} /> - + diff --git a/sdks/js/packages/core/react/components/organization/api-keys/service-user/styles.module.css b/sdks/js/packages/core/react/components/organization/api-keys/service-user/styles.module.css index c3539452f..86efe0229 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/service-user/styles.module.css +++ b/sdks/js/packages/core/react/components/organization/api-keys/service-user/styles.module.css @@ -9,7 +9,7 @@ .content { width: 100%; - padding: var(--space-9) var(--space-11); + padding: var(--rs-space-9) var(--rs-space-11); } .serviceKeyList { @@ -60,3 +60,27 @@ border: 1px solid var(--rs-color-border-base-primary); background: var(--rs-color-background-base-primary-hover); } + +.addDialogContent { + padding: 0; + max-width: 400px; + width: 100%; + z-index: 60; +} + +.overlay { + z-index: 55; + background-color: rgba(104, 112, 118, 0.5); +} + +.addDialogForm { + padding: var(--rs-space-5) var(--rs-space-7); +} + +.addDialogFormContent { + padding: var(--rs-space-9) var(--rs-space-7); +} + +.addDialogFormBtnWrapper { + padding: var(--rs-space-5); +} diff --git a/sdks/js/packages/core/react/components/organization/api-keys/styles.module.css b/sdks/js/packages/core/react/components/organization/api-keys/styles.module.css index e7a6b75d3..51f30967c 100644 --- a/sdks/js/packages/core/react/components/organization/api-keys/styles.module.css +++ b/sdks/js/packages/core/react/components/organization/api-keys/styles.module.css @@ -5,11 +5,11 @@ .content { width: 100%; - padding: var(--space-9) var(--space-11); + padding: var(--rs-space-9) var(--rs-space-11); } .stateContent { - padding: var(--space-15) var(--space-11); + padding: var(--rs-space-15) var(--rs-space-11); } .flex1 { @@ -43,7 +43,7 @@ } .addDialogFormContent { - padding: var(--space-9) var(--space-7); + padding: var(--rs-space-9) var(--rs-space-7); } .addDialogFormBtnWrapper { diff --git a/sdks/js/packages/core/react/components/organization/profile.tsx b/sdks/js/packages/core/react/components/organization/profile.tsx index 6201117e2..f9e0ec72d 100644 --- a/sdks/js/packages/core/react/components/organization/profile.tsx +++ b/sdks/js/packages/core/react/components/organization/profile.tsx @@ -63,5 +63,6 @@ declare module '@tanstack/react-router' { interface HistoryState { token?: V1Beta1ServiceUserToken; + refetch?: boolean; } } diff --git a/sdks/js/packages/core/react/components/organization/routes.tsx b/sdks/js/packages/core/react/components/organization/routes.tsx index 8b10257cb..701ede8d0 100644 --- a/sdks/js/packages/core/react/components/organization/routes.tsx +++ b/sdks/js/packages/core/react/components/organization/routes.tsx @@ -44,6 +44,8 @@ import MemberRemoveConfirm from './members/MemberRemoveConfirm'; import APIKeys from './api-keys'; import { AddServiceAccount } from './api-keys/add'; import ServiceUserPage from './api-keys/service-user'; +import { DeleteServiceAccount } from './api-keys/delete'; +import { DeleteServiceAccountKey } from './api-keys/service-user/delete'; export interface CustomScreen { name: string; @@ -308,12 +310,24 @@ const addServiceAccountRoute = createRoute({ component: AddServiceAccount }); +const deleteServiceAccountRoute = createRoute({ + getParentRoute: () => apiKeysRoute, + path: '/$id/delete', + component: DeleteServiceAccount +}); + const serviceAccountRoute = createRoute({ getParentRoute: () => rootRoute, path: '/api-keys/$id', component: ServiceUserPage }); +const deleteServiceAccountKeyRoute = createRoute({ + getParentRoute: () => serviceAccountRoute, + path: '/key/$tokenId/delete', + component: DeleteServiceAccountKey +}); + interface getRootTreeOptions { customScreens?: CustomScreen[]; } @@ -337,8 +351,11 @@ export function getRootTree({ customScreens = [] }: getRootTreeOptions) { billingRoute.addChildren([switchBillingCycleModalRoute]), plansRoute.addChildren([planDowngradeRoute]), tokensRoute, - apiKeysRoute.addChildren([addServiceAccountRoute]), - serviceAccountRoute, + apiKeysRoute.addChildren([ + addServiceAccountRoute, + deleteServiceAccountRoute + ]), + serviceAccountRoute.addChildren([deleteServiceAccountKeyRoute]), ...customScreens.map(cc => createRoute({ path: cc.path,