Skip to content

CDN config UI v1 #10019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions frontend/orval.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* How to generate OpenAPI client
*
* For now we only use generated types (src/openapi/models).
* Run `yarn gen:api` to generate the client.
* We may use methods (src/openapi/apis) for new features in the future.
*/
module.exports = {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel this should be a separate PR fixing orval config.

unleashApi: {
output: {
mode: 'tags',
workspace: 'src/openapi',
target: 'apis',
schemas: 'models',
client: 'swr',
clean: true,
// mock: true,
override: {
mutator: {
path: './fetcher.ts',
name: 'fetcher',
},
header: () => [
'Generated by Orval',
'Do not edit manually.',
'See `gen:api` script in package.json',
],
},
},
input: {
target:
process.env.UNLEASH_OPENAPI_URL ||
'http://localhost:4242/docs/openapi.json',
},
hooks: {
afterAllFilesWrite: './scripts/clean_orval_generated.sh',
},
},
};
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"ts:check": "tsc",
"e2e": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --no-experimental-fetch\" yarn run cypress open --config baseUrl='http://localhost:3000' --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all",
"e2e:oss": "yarn --cwd frontend run cypress run --spec \"cypress/oss/**/*.spec.ts\" --config baseUrl=\"http://localhost:${EXPOSED_PORT:-4242}\" --env AUTH_USER=admin,AUTH_PASSWORD=unleash4all",
"gen:api": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --no-experimental-fetch\" orval --config orval.config.js",
"gen:api": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --no-experimental-fetch\" orval --config orval.config.cjs",
"gen:api:demo": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --no-experimental-fetch\" UNLEASH_OPENAPI_URL=https://app.unleash-hosted.com/demo/docs/openapi.json yarn run gen:api",
"gen:api:sandbox": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --no-experimental-fetch\" UNLEASH_OPENAPI_URL=https://sandbox.getunleash.io/demo2/docs/openapi.json yarn run gen:api",
"gen:api:clean": "yarn gen:api && rm -rf src/openapi/apis && sed -i.bak '1q' src/openapi/index.ts && rm src/openapi/index.ts.bak"
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/component/admin/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import NotFound from 'component/common/NotFound/NotFound';
import { Banners } from './banners/Banners.tsx';
import { License } from './license/License.tsx';
import { AdminHome } from './AdminHome.tsx';
import { CdnAdmin } from './cdn/CdnAdmin.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
import { CreateCdnToken } from './cdn/CreateCdnToken/CreateCdnToken.tsx';

export const Admin = () => {
const isCdnEnabled = useUiFlag('cdnConfig');

return (
<>
<Routes>
Expand All @@ -39,6 +44,15 @@ export const Admin = () => {
<Route path='banners' element={<Banners />} />
<Route path='license' element={<License />} />
<Route path='cors' element={<CorsAdmin />} />
{isCdnEnabled ? (
<Route path='cdn' element={<CdnAdmin />} />
) : null}
{isCdnEnabled ? (
<Route
path='cdn/create-token'
element={<CreateCdnToken />}
/>
) : null}
<Route path='auth/*' element={<AuthSettings />} />
<Route
path='admin-invoices'
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/component/admin/adminRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ export const adminRoutes: INavigationMenuItem[] = [
menu: { adminSettings: true },
group: 'access',
},
{
path: '/admin/cdn',
title: 'CDN access',
menu: { adminSettings: true },
group: 'access',
flag: 'cdnConfig',
},

// Single sign-on/login
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface IProjectSelectorProps {
errors: { [key: string]: string };
clearErrors: (error?: ApiTokenFormErrorType) => void;
}

export const ProjectSelector = ({
type,
projects,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
CREATE_CLIENT_API_TOKEN,
CREATE_FRONTEND_API_TOKEN,
} from '@server/types/permissions';
import { Box } from '@mui/material';
import { ApiTokenDocs } from '../ApiTokenDocs/ApiTokenDocs.tsx';

export const ApiTokenPage = () => {
const { tokens, loading, refetch } = useApiTokens();
Expand Down Expand Up @@ -96,6 +98,11 @@ export const ApiTokenPage = () => {
/>
}
>
{rows.length > 0 ? (
<Box sx={{ mb: 4 }}>
<ApiTokenDocs />
</Box>
) : null}
Comment on lines +101 to +105
Copy link
Contributor

Choose a reason for hiding this comment

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

This refactoring makes sense but I'd do it as a different PR

<ApiTokenTable
loading={loading}
headerGroups={headerGroups}
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/component/admin/cdn/ApiTokenDocs/ApiTokenDocs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Alert, Box, IconButton, styled, Tooltip } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import CopyIcon from '@mui/icons-material/FileCopy';
import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';

const GridContainer = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'auto auto 1fr',
gridAutoRows: 'min-content',
alignItems: 'center',
gap: theme.spacing(1),
marginTop: theme.spacing(1.5),
}));
const GridItem = Box;

export const ApiTokenDocs = () => {
const { uiConfig } = useUiConfig();
const { setToastData } = useToast();

const onCopyToClipboard = (url: string) => () => {
copy(url);
setToastData({
type: 'success',
text: 'Copied to clipboard',
});
};

const clientApiUrl = `${uiConfig.unleashUrl}/api/`;
const frontendApiUrl = `${uiConfig.unleashUrl}/api/frontend/`;

return (
<Alert severity='info'>
<p>
Read the{' '}
<a
href='https://docs.getunleash.io/reference/sdks'
target='_blank'
rel='noreferrer'
>
SDK overview
</a>{' '}
to connect Unleash to your application. Please note it can take
up to <strong>1 minute</strong> before a new API key is
activated.
</p>
<GridContainer>
<GridItem>
<strong>CLIENT API URL: </strong>
</GridItem>
<GridItem>
<pre style={{ display: 'inline' }}>{clientApiUrl}</pre>
</GridItem>
<GridItem>
<Tooltip title='Copy URL' arrow>
<IconButton
onClick={onCopyToClipboard(clientApiUrl)}
size='small'
>
<CopyIcon />
</IconButton>
</Tooltip>
</GridItem>
<GridItem>
<strong>FRONTEND API URL: </strong>
</GridItem>
<GridItem>
<pre style={{ display: 'inline' }}>{frontendApiUrl}</pre>
</GridItem>
<GridItem>
<Tooltip title='Copy URL' arrow>
<IconButton
onClick={onCopyToClipboard(frontendApiUrl)}
size='small'
>
<CopyIcon />
</IconButton>
</Tooltip>
</GridItem>
</GridContainer>
</Alert>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Box, Button, styled } from '@mui/material';
import Input from '../../../common/Input/Input.tsx';
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect.tsx';

export const StyledContainer = styled('div')(() => ({
maxWidth: '400px',
}));

export const StyledForm = styled('form')(() => ({
display: 'flex',
flexDirection: 'column',
height: '100%',
}));

export const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));

export const StyledSelectInput = styled(GeneralSelect)(({ theme }) => ({
marginBottom: theme.spacing(2),
minWidth: '400px',
[theme.breakpoints.down('sm')]: {
minWidth: '379px',
},
}));

export const StyledInputDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
}));

export const StyledInputLabel = styled('label')(({ theme }) => ({
marginBottom: theme.spacing(1),
}));

export const CancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));

export const StyledBox = styled(Box)({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
});
49 changes: 49 additions & 0 deletions frontend/src/component/admin/cdn/ApiTokenForm/ApiTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Alert, Link } from '@mui/material';
import type React from 'react';
import type { ReactNode } from 'react';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CancelButton, StyledBox, StyledForm } from './ApiTokenForm.styles';

interface IApiTokenFormProps {
handleSubmit: (e: any) => void;
handleCancel: () => void;
mode: 'Create' | 'Edit';
actions?: ReactNode;
children?: React.ReactNode;
}

const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
children,
actions,
handleSubmit,
handleCancel,
}) => {
const { uiConfig } = useUiConfig();

const isUnleashCloud = Boolean(uiConfig?.flags?.UNLEASH_CLOUD);

return (
<StyledForm onSubmit={handleSubmit}>
<ConditionallyRender
condition={isUnleashCloud}
show={
<Alert severity='info' sx={{ mb: 4 }}>
Please be aware of our{' '}
<Link href='https://www.getunleash.io/fair-use-policy'>
fair use policy
</Link>
.
</Alert>
}
/>
{children}
<StyledBox>
{actions}
<CancelButton onClick={handleCancel}>Cancel</CancelButton>
</StyledBox>
</StyledForm>
);
};

export default ApiTokenForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type React from 'react';
import { StyledInput, StyledInputDescription } from '../ApiTokenForm.styles';
import type { ApiTokenFormErrorType } from '../../useCdnTokenForm.ts';

interface ITokenInfoProps {
tokenName: string;
setTokenName: React.Dispatch<React.SetStateAction<string>>;

errors: { [key: string]: string };
clearErrors: (error?: ApiTokenFormErrorType) => void;
}
export const TokenInfo = ({
tokenName,
setTokenName,
errors,
clearErrors,
}: ITokenInfoProps) => {
return (
<>
<StyledInputDescription>
What would you like to call this token?
</StyledInputDescription>
<StyledInput
value={tokenName}
name='tokenName'
onChange={(e) => setTokenName(e.target.value)}
label='Token name'
error={errors.tokenName !== undefined}
errorText={errors.tokenName}
onFocus={() => clearErrors('tokenName')}
autoFocus
/>
</>
);
};
22 changes: 22 additions & 0 deletions frontend/src/component/admin/cdn/CdnAdmin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { ADMIN } from '@server/types/permissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { CdnTokenPage } from './CdnTokenPage/CdnTokenPage.tsx';

export const CdnAdmin = () => (
<div>
<PermissionGuard permissions={[ADMIN]}>
<CdnPage />
</PermissionGuard>
</div>
);

const CdnPage = () => {
const { loading } = useUiConfig();

if (loading) {
return null;
}

return <CdnTokenPage />;
};
Loading