mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: Onboarding connect api token generation (#8054)
This commit is contained in:
parent
29af716952
commit
6a51a0b14a
@ -5,12 +5,20 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
type Theme,
|
type Theme,
|
||||||
Typography,
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { SingleSelectConfigButton } from '../common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton';
|
import { SingleSelectConfigButton } from '../common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton';
|
||||||
import EnvironmentsIcon from '@mui/icons-material/CloudCircle';
|
import EnvironmentsIcon from '@mui/icons-material/CloudCircle';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
import CodeIcon from '@mui/icons-material/Code';
|
||||||
|
import { useProjectApiTokens } from 'hooks/api/getters/useProjectApiTokens/useProjectApiTokens';
|
||||||
|
import { ArcherContainer, ArcherElement } from 'react-archer';
|
||||||
|
import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { parseToken } from './parseToken';
|
||||||
|
|
||||||
interface IConnectSDKDialogProps {
|
interface IConnectSDKDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -19,10 +27,11 @@ interface IConnectSDKDialogProps {
|
|||||||
environments: string[];
|
environments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConceptsDefinitions = styled('div')(({ theme }) => ({
|
const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.background.sidebar,
|
backgroundColor: theme.palette.background.sidebar,
|
||||||
padding: theme.spacing(8),
|
padding: theme.spacing(6),
|
||||||
flexBasis: '30%',
|
flex: 0,
|
||||||
|
minWidth: '400px',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const IconStyle = ({ theme }: { theme: Theme }) => ({
|
const IconStyle = ({ theme }: { theme: Theme }) => ({
|
||||||
@ -59,7 +68,7 @@ export const APIKeyGeneration = styled('div')(({ theme }) => ({
|
|||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexBasis: '70%',
|
flex: 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const SpacedContainer = styled('div')(({ theme }) => ({
|
export const SpacedContainer = styled('div')(({ theme }) => ({
|
||||||
@ -106,15 +115,198 @@ const NextStepSectionSpacedContainer = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(3, 8, 3, 8),
|
padding: theme.spacing(3, 8, 3, 8),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const SecretExplanation = styled('div')(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.elevation1,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SecretExplanationDescription = styled('div')(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.contrastText,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
flex: 1,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ConceptsDefinitions = () => (
|
||||||
|
<ConceptsDefinitionsWrapper>
|
||||||
|
<ConceptItem>
|
||||||
|
<StyledProjectIcon />
|
||||||
|
<Box>
|
||||||
|
<ConceptSummary>Flags live in projects</ConceptSummary>
|
||||||
|
<ConceptDetails>
|
||||||
|
Projects are containers for feature flags. When you create a
|
||||||
|
feature flag it will belong to the project you create it in.
|
||||||
|
</ConceptDetails>
|
||||||
|
</Box>
|
||||||
|
</ConceptItem>
|
||||||
|
<ConceptItem>
|
||||||
|
<StyledEnvironmentsIcon />
|
||||||
|
<Box>
|
||||||
|
<ConceptSummary>
|
||||||
|
Flags have configuration in environments
|
||||||
|
</ConceptSummary>
|
||||||
|
<ConceptDetails>
|
||||||
|
In Unleash you can have multiple environments. Each feature
|
||||||
|
flag will have different configuration in every environment.
|
||||||
|
</ConceptDetails>
|
||||||
|
</Box>
|
||||||
|
</ConceptItem>
|
||||||
|
<ConceptItem>
|
||||||
|
<StyledCodeIcon />
|
||||||
|
<Box>
|
||||||
|
<ConceptSummary>
|
||||||
|
SDKs connect to Unleash to retrieve configuration
|
||||||
|
</ConceptSummary>
|
||||||
|
<ConceptDetails>
|
||||||
|
When you connect an SDK to Unleash it will use the API key
|
||||||
|
to deduce which feature flags to retrieve and from which
|
||||||
|
environment to retrieve configuration.
|
||||||
|
</ConceptDetails>
|
||||||
|
</Box>
|
||||||
|
</ConceptItem>
|
||||||
|
</ConceptsDefinitionsWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TokenExplanationBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginTop: theme.spacing(8),
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TokenExplanation = ({
|
||||||
|
project,
|
||||||
|
environment,
|
||||||
|
secret,
|
||||||
|
}: { project: string; environment: string; secret: string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArcherContainer
|
||||||
|
strokeColor={theme.palette.secondary.border}
|
||||||
|
endMarker={false}
|
||||||
|
lineStyle='curve'
|
||||||
|
>
|
||||||
|
<SecretExplanation>
|
||||||
|
<Box sx={{ wordBreak: 'break-all' }}>
|
||||||
|
<ArcherElement id='project'>
|
||||||
|
<span>{project}</span>
|
||||||
|
</ArcherElement>
|
||||||
|
:
|
||||||
|
<ArcherElement id='environment'>
|
||||||
|
<span>{environment}</span>
|
||||||
|
</ArcherElement>
|
||||||
|
.
|
||||||
|
<ArcherElement id='secret'>
|
||||||
|
<span>{secret}</span>
|
||||||
|
</ArcherElement>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isLargeScreen ? (
|
||||||
|
<TokenExplanationBox>
|
||||||
|
<ArcherElement
|
||||||
|
id='project-description'
|
||||||
|
relations={[
|
||||||
|
{
|
||||||
|
targetId: 'project',
|
||||||
|
targetAnchor: 'bottom',
|
||||||
|
sourceAnchor: 'top',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SecretExplanationDescription>
|
||||||
|
The project this API key will retrieve feature
|
||||||
|
flags from
|
||||||
|
</SecretExplanationDescription>
|
||||||
|
</ArcherElement>
|
||||||
|
<ArcherElement
|
||||||
|
id='environment-description'
|
||||||
|
relations={[
|
||||||
|
{
|
||||||
|
targetId: 'environment',
|
||||||
|
targetAnchor: 'bottom',
|
||||||
|
sourceAnchor: 'top',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SecretExplanationDescription>
|
||||||
|
The environment the API key will retrieve
|
||||||
|
feature flag configuration from
|
||||||
|
</SecretExplanationDescription>
|
||||||
|
</ArcherElement>
|
||||||
|
<ArcherElement
|
||||||
|
id='secreat-description'
|
||||||
|
relations={[
|
||||||
|
{
|
||||||
|
targetId: 'secret',
|
||||||
|
targetAnchor: 'bottom',
|
||||||
|
sourceAnchor: 'top',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SecretExplanationDescription>
|
||||||
|
The API key secret
|
||||||
|
</SecretExplanationDescription>
|
||||||
|
</ArcherElement>
|
||||||
|
</TokenExplanationBox>
|
||||||
|
) : null}
|
||||||
|
</SecretExplanation>
|
||||||
|
</ArcherContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChooseEnvironment = ({
|
||||||
|
environments,
|
||||||
|
onSelect,
|
||||||
|
currentEnvironment,
|
||||||
|
}: {
|
||||||
|
environments: string[];
|
||||||
|
currentEnvironment: string;
|
||||||
|
onSelect: (env: string) => void;
|
||||||
|
}) => {
|
||||||
|
const longestEnv = Math.max(
|
||||||
|
...environments.map((environment) => environment.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SingleSelectConfigButton
|
||||||
|
tooltip={{ header: '' }}
|
||||||
|
description='Select the environment where API key will be created'
|
||||||
|
options={environments.map((environment) => ({
|
||||||
|
label: environment,
|
||||||
|
value: environment,
|
||||||
|
}))}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
onSelect(value);
|
||||||
|
}}
|
||||||
|
button={{
|
||||||
|
label: currentEnvironment,
|
||||||
|
icon: <EnvironmentsIcon />,
|
||||||
|
labelWidth: `${longestEnv + 5}ch`,
|
||||||
|
}}
|
||||||
|
search={{
|
||||||
|
label: 'Filter project mode options',
|
||||||
|
placeholder: 'Select project mode',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ConnectSDKDialog = ({
|
export const ConnectSDKDialog = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
environments,
|
environments,
|
||||||
|
project,
|
||||||
}: IConnectSDKDialogProps) => {
|
}: IConnectSDKDialogProps) => {
|
||||||
const [environment, setEnvironment] = useState('');
|
const [environment, setEnvironment] = useState('');
|
||||||
const longestEnv = Math.max(
|
|
||||||
...environments.map((environment) => environment.length),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environments.length > 0) {
|
if (environments.length > 0) {
|
||||||
@ -122,6 +314,34 @@ export const ConnectSDKDialog = ({
|
|||||||
}
|
}
|
||||||
}, [JSON.stringify(environments)]);
|
}, [JSON.stringify(environments)]);
|
||||||
|
|
||||||
|
const { tokens, refetch: refreshTokens } = useProjectApiTokens(project);
|
||||||
|
const { createToken, loading: creatingToken } = useProjectApiTokensApi();
|
||||||
|
const currentEnvironmentToken = tokens.find(
|
||||||
|
(token) => token.environment === environment,
|
||||||
|
);
|
||||||
|
const parsedToken = parseToken(currentEnvironmentToken?.secret);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg'));
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const generateAPIKey = async () => {
|
||||||
|
try {
|
||||||
|
await createToken(
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
type: 'CLIENT',
|
||||||
|
projects: [project],
|
||||||
|
username: 'onboarding-api-key',
|
||||||
|
},
|
||||||
|
project,
|
||||||
|
);
|
||||||
|
refreshTokens();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDialog open={open} onClose={onClose}>
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
@ -130,49 +350,54 @@ export const ConnectSDKDialog = ({
|
|||||||
<Typography variant='h2'>
|
<Typography variant='h2'>
|
||||||
Connect an SDK to Unleash
|
Connect an SDK to Unleash
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box sx={{ mt: 4 }}>
|
||||||
<SectionHeader>Environment</SectionHeader>
|
<SectionHeader>Environment</SectionHeader>
|
||||||
<SectionDescription>
|
<SectionDescription>
|
||||||
The environment SDK will connect to in order to
|
The environment SDK will connect to in order to
|
||||||
retrieve configuration.
|
retrieve configuration.
|
||||||
</SectionDescription>
|
</SectionDescription>
|
||||||
{environments.length > 0 ? (
|
{environments.length > 0 ? (
|
||||||
<SingleSelectConfigButton
|
<ChooseEnvironment
|
||||||
tooltip={{ header: '' }}
|
environments={environments}
|
||||||
description='Select the environment where API key will be created'
|
currentEnvironment={environment}
|
||||||
options={environments.map(
|
onSelect={setEnvironment}
|
||||||
(environment) => ({
|
|
||||||
label: environment,
|
|
||||||
value: environment,
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
setEnvironment(value);
|
|
||||||
}}
|
|
||||||
button={{
|
|
||||||
label: environment,
|
|
||||||
icon: <EnvironmentsIcon />,
|
|
||||||
labelWidth: `${longestEnv + 5}ch`,
|
|
||||||
}}
|
|
||||||
search={{
|
|
||||||
label: 'Filter project mode options',
|
|
||||||
placeholder: 'Select project mode',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<SectionHeader>API Key</SectionHeader>
|
<SectionHeader>API Key</SectionHeader>
|
||||||
<SectionDescription>
|
{parsedToken ? (
|
||||||
You currently have no active API keys for this
|
<SectionDescription>
|
||||||
project/environment combination. You'll need to
|
Here is your generated API key. We will use
|
||||||
generate and API key in order to proceed with
|
it to connect to the{' '}
|
||||||
connecting your SDK.
|
<b>{parsedToken.project}</b> project in the{' '}
|
||||||
</SectionDescription>
|
<b>{parsedToken.environment}</b>{' '}
|
||||||
<Button variant='contained'>
|
environment.
|
||||||
Generate API Key
|
</SectionDescription>
|
||||||
</Button>
|
) : (
|
||||||
|
<SectionDescription>
|
||||||
|
You currently have no active API keys for
|
||||||
|
this project/environment combination. You'll
|
||||||
|
need to generate and API key in order to
|
||||||
|
proceed with connecting your SDK.
|
||||||
|
</SectionDescription>
|
||||||
|
)}
|
||||||
|
{parsedToken ? (
|
||||||
|
<TokenExplanation
|
||||||
|
project={parsedToken.project}
|
||||||
|
environment={parsedToken.environment}
|
||||||
|
secret={parsedToken.secret}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
disabled={creatingToken}
|
||||||
|
onClick={generateAPIKey}
|
||||||
|
>
|
||||||
|
Generate API Key
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</SpacedContainer>
|
</SpacedContainer>
|
||||||
|
|
||||||
@ -185,49 +410,8 @@ export const ConnectSDKDialog = ({
|
|||||||
</NextStepSectionSpacedContainer>
|
</NextStepSectionSpacedContainer>
|
||||||
</NextStepSection>
|
</NextStepSection>
|
||||||
</APIKeyGeneration>
|
</APIKeyGeneration>
|
||||||
<ConceptsDefinitions>
|
|
||||||
<ConceptItem>
|
{isLargeScreen ? <ConceptsDefinitions /> : null}
|
||||||
<StyledProjectIcon />
|
|
||||||
<Box>
|
|
||||||
<ConceptSummary>
|
|
||||||
Flags live in projects
|
|
||||||
</ConceptSummary>
|
|
||||||
<ConceptDetails>
|
|
||||||
Projects are containers for feature flags. When
|
|
||||||
you create a feature flag it will belong to the
|
|
||||||
project you create it in.
|
|
||||||
</ConceptDetails>
|
|
||||||
</Box>
|
|
||||||
</ConceptItem>
|
|
||||||
<ConceptItem>
|
|
||||||
<StyledEnvironmentsIcon />
|
|
||||||
<Box>
|
|
||||||
<ConceptSummary>
|
|
||||||
Flags have configuration in environments
|
|
||||||
</ConceptSummary>
|
|
||||||
<ConceptDetails>
|
|
||||||
In Unleash you can have multiple environments.
|
|
||||||
Each feature flag will have different
|
|
||||||
configuration in every environment.
|
|
||||||
</ConceptDetails>
|
|
||||||
</Box>
|
|
||||||
</ConceptItem>
|
|
||||||
<ConceptItem>
|
|
||||||
<StyledCodeIcon />
|
|
||||||
<Box>
|
|
||||||
<ConceptSummary>
|
|
||||||
SDKs connect to Unleash to retrieve
|
|
||||||
configuration
|
|
||||||
</ConceptSummary>
|
|
||||||
<ConceptDetails>
|
|
||||||
When you connect an SDK to Unleash it will use
|
|
||||||
the API key to deduce which feature flags to
|
|
||||||
retrieve and from which environment to retrieve
|
|
||||||
configuration.
|
|
||||||
</ConceptDetails>
|
|
||||||
</Box>
|
|
||||||
</ConceptItem>
|
|
||||||
</ConceptsDefinitions>
|
|
||||||
</Box>
|
</Box>
|
||||||
</StyledDialog>
|
</StyledDialog>
|
||||||
);
|
);
|
||||||
|
59
frontend/src/component/onboarding/parseToken.test.ts
Normal file
59
frontend/src/component/onboarding/parseToken.test.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { parseToken } from './parseToken';
|
||||||
|
|
||||||
|
describe('parseToken', () => {
|
||||||
|
test('should return null if token is undefined', () => {
|
||||||
|
const result = parseToken(undefined);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if token is an empty string', () => {
|
||||||
|
const result = parseToken('');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the correct object when a valid token is provided', () => {
|
||||||
|
const token = 'project123:env456.secret789';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toEqual({
|
||||||
|
project: 'project123',
|
||||||
|
environment: 'env456',
|
||||||
|
secret: 'secret789',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token does not have a colon', () => {
|
||||||
|
const token = 'project123env456.secret789';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token does not have a period', () => {
|
||||||
|
const token = 'project123:env456secret789';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token has an incomplete project part', () => {
|
||||||
|
const token = ':env456.secret789';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token has an incomplete environment part', () => {
|
||||||
|
const token = 'project123:.secret789';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token has an incomplete secret part', () => {
|
||||||
|
const token = 'project123:env456.';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null if the token has extra parts', () => {
|
||||||
|
const token = 'project123:env456.secret789.extra';
|
||||||
|
const result = parseToken(token);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
20
frontend/src/component/onboarding/parseToken.ts
Normal file
20
frontend/src/component/onboarding/parseToken.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const parseToken = (
|
||||||
|
token?: string,
|
||||||
|
): { project: string; environment: string; secret: string } | null => {
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const [project, rest] = token.split(':', 2);
|
||||||
|
if (!rest) return null;
|
||||||
|
|
||||||
|
const [environment, secret, ...extra] = rest.split('.');
|
||||||
|
|
||||||
|
if (project && environment && secret && extra.length === 0) {
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
environment,
|
||||||
|
secret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -94,7 +94,7 @@ export const theme = {
|
|||||||
contrastText: colors.grey[50], // Color used for content when primary.main is used as a background
|
contrastText: colors.grey[50], // Color used for content when primary.main is used as a background
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
// Used for purple badges and puple light elements
|
// Used for purple badges and purple light elements
|
||||||
main: colors.purple[800],
|
main: colors.purple[800],
|
||||||
light: colors.purple[50],
|
light: colors.purple[50],
|
||||||
dark: colors.purple[900], // Color used for text
|
dark: colors.purple[900], // Color used for text
|
||||||
@ -150,7 +150,7 @@ export const theme = {
|
|||||||
default: colors.grey[50],
|
default: colors.grey[50],
|
||||||
application: colors.grey[300],
|
application: colors.grey[300],
|
||||||
sidebar: colors.purple[800],
|
sidebar: colors.purple[800],
|
||||||
alternative: colors.purple[800], // used on the dark theme to shwitch primary main to a darker shade
|
alternative: colors.purple[800], // used on the dark theme to switch primary main to a darker shade
|
||||||
elevation1: colors.grey[100],
|
elevation1: colors.grey[100],
|
||||||
elevation2: colors.grey[200],
|
elevation2: colors.grey[200],
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user