Skip to content

Commit

Permalink
feat(sdk): add screen to manage service account and API keys (#815)
Browse files Browse the repository at this point in the history
* chore: update style in sdk demo

* feat: add APIKeys placeholder page

* feat: add showAPIKeys props to IAM dialog

* feat: add route for api keys page

* feat: add sidebar link to api keys page

* fix: pass showAPIKeys to helper function

* chore: pass showAPIKeys from sdk-demo app

* feat: update apsara version to v0.23.0

* feat: add empty state to API Keys screen

* chore: update frontier in sdk demo

* revert: sdk demo changes

* refactor: create seperate component for NoServiceAccounts

* feat: check permission in API keys screen

* chore: update page heading
  • Loading branch information
rsbh authored Nov 20, 2024
1 parent 2c3acc1 commit 594e858
Show file tree
Hide file tree
Showing 16 changed files with 6,606 additions and 8,033 deletions.
4 changes: 2 additions & 2 deletions sdks/js/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"@radix-ui/react-icons": "^1.3.0",
"@raystack/apsara": "^0.20.4",
"@raystack/apsara": "^0.23.0",
"@raystack/eslint-config": "workspace:^",
"@raystack/frontier-tsconfig": "workspace:^",
"@size-limit/preset-small-lib": "^8.2.6",
Expand Down Expand Up @@ -102,7 +102,7 @@
"yup": "^1.2.0"
},
"peerDependencies": {
"@raystack/apsara": ">=0.20.4",
"@raystack/apsara": ">=0.23.0",
"react": "^18.2.0"
},
"peerDependenciesMeta": {
Expand Down
5 changes: 5 additions & 0 deletions sdks/js/packages/core/react/assets/key.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions sdks/js/packages/core/react/components/organization/api-keys/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Flex, Text, EmptyState, Button } from '@raystack/apsara/v1';
import styles from './styles.module.css';
import keyIcon from '~/react/assets/key.svg';
import { Image } from '@raystack/apsara';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { DEFAULT_API_PLATFORM_APP_NAME } from '~/react/utils/constants';
import { FrontierClientAPIPlatformOptions } from '~/shared/types';
import { useMemo } from 'react';
import { PERMISSIONS, shouldShowComponent } from '~/utils';
import { usePermissions } from '~/react/hooks/usePermissions';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';

const NoServiceAccounts = ({
config
}: {
config?: FrontierClientAPIPlatformOptions;
}) => {
const appName = config?.appName || DEFAULT_API_PLATFORM_APP_NAME;
return (
<EmptyState
icon={
<Image
// @ts-ignore
src={keyIcon}
alt="keyIcon"
/>
}
heading="No service account"
subHeading={`Create a new account to use the APIs of ${appName}`}
primaryAction={
<Button
data-test-id="frontier-sdk-new-service-account-btn"
variant="secondary"
>
Create new service account
</Button>
}
/>
);
};

const NoAccess = () => {
return (
<EmptyState
icon={<ExclamationTriangleIcon />}
heading="Restricted Access"
subHeading={`Admin access required, please reach out to your admin incase you want to generate a key.`}
/>
);
};

const useAccess = (orgId?: string) => {
const resource = `app/organization:${orgId}`;
const listOfPermissionsToCheck = useMemo(() => {
return [
{
permission: PERMISSIONS.UpdatePermission,
resource: resource
}
];
}, [resource]);

const { permissions, isFetching: isPermissionsFetching } = usePermissions(
listOfPermissionsToCheck,
!!orgId
);

const canUpdateWorkspace = useMemo(() => {
return shouldShowComponent(
permissions,
`${PERMISSIONS.UpdatePermission}::${resource}`
);
}, [permissions, resource]);

return {
isPermissionsFetching,
canUpdateWorkspace
};
};

export default function ApiKeys() {
const {
activeOrganization: organization,
isActiveOrganizationLoading,
config
} = useFrontier();

const { isPermissionsFetching, canUpdateWorkspace } = useAccess(
organization?.id
);

// TODO: show skeleton loader for Keys List
const isLoading = isActiveOrganizationLoading || isPermissionsFetching;

Check warning on line 93 in sdks/js/packages/core/react/components/organization/api-keys/index.tsx

View workflow job for this annotation

GitHub Actions / JS SDK Lint

'isLoading' is assigned a value but never used

return (
<Flex direction="column" style={{ width: '100%' }}>
<Flex className={styles.header}>
<Text size={6}>API</Text>
</Flex>
<Flex justify="center" align="center" className={styles.content}>
{canUpdateWorkspace ? (
<NoServiceAccounts config={config.apiPlatform} />
) : (
<NoAccess />
)}
</Flex>
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.header {
padding: var(--rs-space-5) var(--rs-space-11);
border-bottom: 1px solid var(--rs-color-border-base-primary);
}

.content {
padding: var(--space-15) var(--space-11);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const router = createRouter({
organizationId: '',
showBilling: false,
showTokens: false,
showAPIKeys: false,
showPreferences: false,
customRoutes: { Organization: [], User: [] }
}
Expand All @@ -25,6 +26,7 @@ export const OrganizationProfile = ({
defaultRoute = '/',
showBilling = false,
showTokens = false,
showAPIKeys = false,
showPreferences = false,
hideToast = false,
customScreens = []
Expand All @@ -44,6 +46,7 @@ export const OrganizationProfile = ({
organizationId,
showBilling,
showTokens,
showAPIKeys,
hideToast,
showPreferences,
customRoutes
Expand Down
10 changes: 10 additions & 0 deletions sdks/js/packages/core/react/components/organization/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ConfirmCycleSwitch } from './billing/cycle-switch';
import Plans from './plans';
import ConfirmPlanChange from './plans/confirm-change';
import MemberRemoveConfirm from './members/MemberRemoveConfirm';
import APIKeys from './api-keys';

export interface CustomScreen {
name: string;
Expand All @@ -54,6 +55,7 @@ export interface OrganizationProfileProps {
defaultRoute?: string;
showBilling?: boolean;
showTokens?: boolean;
showAPIKeys?: boolean;
showPreferences?: boolean;
hideToast?: boolean;
customScreens?: CustomScreen[];
Expand All @@ -69,6 +71,7 @@ type RouterContext = Pick<
| 'organizationId'
| 'showBilling'
| 'showTokens'
| 'showAPIKeys'
| 'hideToast'
| 'showPreferences'
> & { customRoutes: CustomRoutes };
Expand Down Expand Up @@ -291,6 +294,12 @@ const tokensRoute = createRoute({
component: Tokens
});

const apiKeysRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/api-keys',
component: APIKeys
});

interface getRootTreeOptions {
customScreens?: CustomScreen[];
}
Expand All @@ -314,6 +323,7 @@ export function getRootTree({ customScreens = [] }: getRootTreeOptions) {
billingRoute.addChildren([switchBillingCycleModalRoute]),
plansRoute.addChildren([planDowngradeRoute]),
tokensRoute,
apiKeysRoute,
...customScreens.map(cc =>
createRoute({
path: cc.path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type CustomRoutes = Array<{ name: string; path: string }>;
interface getOrganizationNavItemsOptions {
showBilling?: boolean;
showTokens?: boolean;
showAPIKeys?: boolean;
canSeeBilling?: boolean;
customRoutes?: CustomRoutes;
}
Expand Down Expand Up @@ -74,6 +75,11 @@ export const getOrganizationNavItems = (
name: 'Plans',
to: '/plans',
show: options?.showBilling
},
{
name: 'API',
to: '/api-keys',
show: options?.showAPIKeys
}
];
const customRoutes = getCustomRoutes(options?.customRoutes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const Sidebar = () => {
organizationId,
showBilling,
showTokens,
showAPIKeys,
showPreferences,
customRoutes
} = useRouteContext({
Expand Down Expand Up @@ -70,9 +71,16 @@ export const Sidebar = () => {
showBilling: showBilling,
canSeeBilling: canSeeBilling,
showTokens: showTokens,
showAPIKeys: showAPIKeys,
customRoutes: customRoutes.Organization
}),
[showBilling, canSeeBilling, showTokens, customRoutes.Organization]
[
showBilling,
canSeeBilling,
showTokens,
showAPIKeys,
customRoutes.Organization
]
);

const userNavItems = useMemo(
Expand Down
6 changes: 3 additions & 3 deletions sdks/js/packages/core/react/contexts/FrontierProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ThemeProvider } from '@raystack/apsara';
import { ThemeProvider } from '@raystack/apsara/v1';
import { FrontierProviderProps } from '../../shared/types';
import { FrontierContextProvider } from './FrontierContext';
import { withMaxAllowedInstancesGuard } from './useMaxAllowedInstancesGuard';
Expand All @@ -7,14 +7,14 @@ export const multipleFrontierProvidersError =
"Frontier: You've added multiple <FrontierProvider> components in your React component tree. Wrap your components in a single <FrontierProvider>.";

export const FrontierProvider = (props: FrontierProviderProps) => {
const { children, initialState, config, ...options } = props;
const { children, initialState, config, theme, ...options } = props;
return (
<FrontierContextProvider
initialState={initialState}
config={config}
{...options}
>
<ThemeProvider defaultTheme={config?.theme}>{children}</ThemeProvider>
<ThemeProvider {...theme}>{children}</ThemeProvider>
</FrontierContextProvider>
);
};
Expand Down
2 changes: 1 addition & 1 deletion sdks/js/packages/core/react/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '@raystack/apsara/index.css';
import '@raystack/apsara/style.css';
import 'react-loading-skeleton/dist/skeleton.css';
import Amount from './components/helpers/Amount';

Expand Down
2 changes: 2 additions & 0 deletions sdks/js/packages/core/react/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export const INVOICE_STATES = {
PAID: 'paid',
DRAFT: 'draft'
} as const;

export const DEFAULT_API_PLATFORM_APP_NAME = 'Frontier platform';
9 changes: 7 additions & 2 deletions sdks/js/packages/core/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { V1Beta1Organization } from '../api-client';
import { BasePlan } from '../src/types';

import { ThemeProviderProps } from '@raystack/apsara/v1';
export type CustomFetch = typeof fetch;

export interface FrontierClientBillingOptions {
Expand All @@ -15,8 +15,11 @@ export interface FrontierClientBillingOptions {
basePlan?: BasePlan;
}

export interface FrontierClientAPIPlatformOptions {
appName?: string;
}

export interface FrontierClientOptions {
theme?: 'dark' | 'light';
endpoint: string;
redirectSignup?: string;
redirectLogin?: string;
Expand All @@ -25,6 +28,7 @@ export interface FrontierClientOptions {
dateFormat?: string;
shortDateFormat?: string;
billing?: FrontierClientBillingOptions;
apiPlatform?: FrontierClientAPIPlatformOptions;
messages?: {
billing?: {
plan_change?: Record<string, string>;
Expand All @@ -41,4 +45,5 @@ export interface FrontierProviderProps {
children: React.ReactNode;
initialState?: InitialState;
customFetch?: (activeOrg?: V1Beta1Organization) => CustomFetch;
theme?: ThemeProviderProps;
}
1 change: 1 addition & 0 deletions sdks/js/packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "@raystack/frontier-tsconfig/react-library.json",
"compilerOptions": {
"moduleResolution": "bundler",
"paths": {
"~/*": ["./*"]
},
Expand Down
2 changes: 1 addition & 1 deletion sdks/js/packages/sdk-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"typescript": "^5"
},
"packageManager": "pnpm@8.6.10"
}
}
14 changes: 7 additions & 7 deletions sdks/js/packages/sdk-demo/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Link from 'next/link';
import { redirect } from 'next/navigation';
import { useContext, useEffect } from 'react';

import frontierClient from '@/api/frontier'
import frontierClient from '@/api/frontier';

export default function Home() {
const { isAuthorized } = useContext(AuthContext);
Expand All @@ -18,30 +18,30 @@ export default function Home() {
}
}, [isAuthorized]);


async function logout() {
const resp = await frontierClient?.frontierServiceAuthLogout();
if (resp?.status === 200) {
window.location.reload()
window.location.reload();
}
}

return (
<main>
<Flex
justify="center"
align="center"
style={{ height: '100vh', width: '100vw' }}
direction="column"
>
<Button data-test-id='[logout-button]' onClick={logout}>Logout</Button>
<Flex direction="column">
<Button data-test-id="[logout-button]" onClick={logout}>
Logout
</Button>
<Flex direction="row" wrap="wrap">
{organizations.map(org => (
<Flex
key={org.id}
style={{
padding: '16px',
border: '1px solid var(--border-base)',
width: '100%',
margin: '8px'
}}
>
Expand Down
Loading

0 comments on commit 594e858

Please sign in to comment.