diff --git a/frontend/src/component/onboarding/ConnectSDKDialog.tsx b/frontend/src/component/onboarding/ConnectSDKDialog.tsx index e5d597173a..2979804761 100644 --- a/frontend/src/component/onboarding/ConnectSDKDialog.tsx +++ b/frontend/src/component/onboarding/ConnectSDKDialog.tsx @@ -3,22 +3,10 @@ import { Button, Dialog, styled, - type Theme, - Typography, useMediaQuery, useTheme, } from '@mui/material'; -import { SingleSelectConfigButton } from '../common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton'; -import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; -import { useEffect, useState } from 'react'; -import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon'; -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'; +import { GenrateApiKeyConcepts, GeneratApiKey } from './GenerateApiKey'; interface IConnectSDKDialogProps { open: boolean; @@ -27,57 +15,13 @@ interface IConnectSDKDialogProps { environments: string[]; } -const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({ - backgroundColor: theme.palette.background.sidebar, - padding: theme.spacing(6), - flex: 0, - minWidth: '400px', -})); - -const IconStyle = ({ theme }: { theme: Theme }) => ({ - color: theme.palette.primary.contrastText, - fontSize: theme.fontSizes.smallBody, - marginTop: theme.spacing(0.5), -}); - -const StyledProjectIcon = styled(ProjectIcon)(IconStyle); -const StyledEnvironmentsIcon = styled(EnvironmentsIcon)(IconStyle); -const StyledCodeIcon = styled(CodeIcon)(IconStyle); - -const ConceptItem = styled('div')(({ theme }) => ({ - display: 'flex', - gap: theme.spacing(1.5), - alignItems: 'flex-start', - marginTop: theme.spacing(3), -})); - -const ConceptSummary = styled('div')(({ theme }) => ({ - color: theme.palette.primary.contrastText, - fontSize: theme.fontSizes.smallBody, - fontWeight: theme.fontWeight.bold, - marginBottom: theme.spacing(2), -})); - -const ConceptDetails = styled('p')(({ theme }) => ({ - color: theme.palette.primary.contrastText, - fontSize: theme.fontSizes.smallerBody, - marginBottom: theme.spacing(2), -})); - -export const APIKeyGeneration = styled('div')(({ theme }) => ({ +const ConnectSdk = styled('main')(({ theme }) => ({ backgroundColor: theme.palette.background.paper, display: 'flex', flexDirection: 'column', flex: 1, })); -export const SpacedContainer = styled('div')(({ theme }) => ({ - padding: theme.spacing(5, 8, 3, 8), - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(3), -})); - const StyledDialog = styled(Dialog)(({ theme }) => ({ '& .MuiDialog-paper': { borderRadius: theme.shape.borderRadiusLarge, @@ -91,327 +35,52 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({ }, })); -const SectionHeader = styled('div')(({ theme }) => ({ - fontWeight: theme.fontWeight.bold, - marginBottom: theme.spacing(1), - fontSize: theme.fontSizes.bodySize, -})); - -const SectionDescription = styled('p')(({ theme }) => ({ - color: theme.palette.text.secondary, - fontSize: theme.fontSizes.smallBody, - marginBottom: theme.spacing(2), -})); - -const NextStepSection = styled('div')(({ theme }) => ({ +const Navigation = styled('div')(({ theme }) => ({ marginTop: 'auto', borderTop: `1px solid ${theme.palette.divider}}`, -})); - -const NextStepSectionSpacedContainer = styled('div')(({ theme }) => ({ display: 'flex', - justifyContent: 'space-between', + justifyContent: 'flex-end', + gap: theme.spacing(4), alignItems: 'center', 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), +const NextStepSectionSpacedContainer = styled('div')(({ theme }) => ({ display: 'flex', - flexDirection: 'column', + justifyContent: 'flex-end', + gap: theme.spacing(4), alignItems: 'center', + padding: theme.spacing(3, 8, 3, 8), })); -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 = () => ( - - - - - Flags live in projects - - Projects are containers for feature flags. When you create a - feature flag it will belong to the project you create it in. - - - - - - - - Flags have configuration in environments - - - In Unleash you can have multiple environments. Each feature - flag will have different configuration in every environment. - - - - - - - - SDKs connect to Unleash to retrieve configuration - - - 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. - - - - -); - -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 ( - - - - - {project} - - : - - {environment} - - . - - {secret} - - - - {isLargeScreen ? ( - - - - The project this API key will retrieve feature - flags from - - - - - The environment the API key will retrieve - feature flag configuration from - - - - - The API key secret - - - - ) : null} - - - ); -}; - -const ChooseEnvironment = ({ - environments, - onSelect, - currentEnvironment, -}: { - environments: string[]; - currentEnvironment: string; - onSelect: (env: string) => void; -}) => { - const longestEnv = Math.max( - ...environments.map((environment) => environment.length), - ); - - return ( - ({ - label: environment, - value: environment, - }))} - onChange={(value: any) => { - onSelect(value); - }} - button={{ - label: currentEnvironment, - icon: , - labelWidth: `${longestEnv + 5}ch`, - }} - search={{ - label: 'Filter project mode options', - placeholder: 'Select project mode', - }} - /> - ); -}; - export const ConnectSDKDialog = ({ open, onClose, environments, project, }: IConnectSDKDialogProps) => { - const [environment, setEnvironment] = useState(''); - - useEffect(() => { - if (environments.length > 0) { - setEnvironment(environments[0]); - } - }, [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 ( - - - - Connect an SDK to Unleash - - - Environment - - The environment SDK will connect to in order to - retrieve configuration. - - {environments.length > 0 ? ( - - ) : null} - - - - API Key - {parsedToken ? ( - - Here is your generated API key. We will use - it to connect to the{' '} - {parsedToken.project} project in the{' '} - {parsedToken.environment}{' '} - environment. - - ) : ( - - 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. - - )} - {parsedToken ? ( - - ) : ( - - )} - - - - + + + - - Next: Choose SDK and connect - + - - + + - {isLargeScreen ? : null} + {isLargeScreen ? : null} ); diff --git a/frontend/src/component/onboarding/GenerateApiKey.tsx b/frontend/src/component/onboarding/GenerateApiKey.tsx new file mode 100644 index 0000000000..cc7b1e1330 --- /dev/null +++ b/frontend/src/component/onboarding/GenerateApiKey.tsx @@ -0,0 +1,358 @@ +import { useEffect, useState } from 'react'; +import { useProjectApiTokens } from '../../hooks/api/getters/useProjectApiTokens/useProjectApiTokens'; +import useProjectApiTokensApi from '../../hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi'; +import { parseToken } from './parseToken'; +import useToast from '../../hooks/useToast'; +import { formatUnknownError } from '../../utils/formatUnknownError'; +import { + Box, + Button, + styled, + type Theme, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import { SingleSelectConfigButton } from '../common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton'; +import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; +import { ArcherContainer, ArcherElement } from 'react-archer'; +import { ProjectIcon } from '../common/ProjectIcon/ProjectIcon'; +import CodeIcon from '@mui/icons-material/Code'; + +const ChooseEnvironment = ({ + environments, + onSelect, + currentEnvironment, +}: { + environments: string[]; + currentEnvironment: string; + onSelect: (env: string) => void; +}) => { + const longestEnv = Math.max( + ...environments.map((environment) => environment.length), + ); + + return ( + ({ + label: environment, + value: environment, + }))} + onChange={(value: any) => { + onSelect(value); + }} + button={{ + label: currentEnvironment, + icon: , + labelWidth: `${longestEnv + 5}ch`, + }} + search={{ + label: 'Filter project mode options', + placeholder: 'Select project mode', + }} + /> + ); +}; + +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 TokenExplanationBox = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(2), + alignItems: 'flex-start', + marginTop: theme.spacing(8), + flexWrap: 'wrap', +})); + +const SectionHeader = styled('div')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + marginBottom: theme.spacing(1), + fontSize: theme.fontSizes.bodySize, +})); + +const SectionDescription = styled('p')(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.fontSizes.smallBody, + marginBottom: theme.spacing(2), +})); + +const SpacedContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(5, 8, 3, 8), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), +})); + +const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.background.sidebar, + padding: theme.spacing(6), + flex: 0, + minWidth: '400px', +})); + +const ConceptDetails = styled('p')(({ theme }) => ({ + color: theme.palette.primary.contrastText, + fontSize: theme.fontSizes.smallerBody, + marginBottom: theme.spacing(2), +})); + +const IconStyle = ({ theme }: { theme: Theme }) => ({ + color: theme.palette.primary.contrastText, + fontSize: theme.fontSizes.smallBody, + marginTop: theme.spacing(0.5), +}); + +const StyledProjectIcon = styled(ProjectIcon)(IconStyle); +const StyledEnvironmentsIcon = styled(EnvironmentsIcon)(IconStyle); +const StyledCodeIcon = styled(CodeIcon)(IconStyle); + +const ConceptItem = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1.5), + alignItems: 'flex-start', + marginTop: theme.spacing(3), +})); + +const ConceptSummary = styled('div')(({ theme }) => ({ + color: theme.palette.primary.contrastText, + fontSize: theme.fontSizes.smallBody, + fontWeight: theme.fontWeight.bold, + marginBottom: theme.spacing(2), +})); + +const TokenExplanation = ({ + project, + environment, + secret, +}: { project: string; environment: string; secret: string }) => { + const theme = useTheme(); + const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg')); + + return ( + + + + + {project} + + : + + {environment} + + . + + {secret} + + + + {isLargeScreen ? ( + + + + The project this API key will retrieve feature + flags from + + + + + The environment the API key will retrieve + feature flag configuration from + + + + + The API key secret + + + + ) : null} + + + ); +}; + +export const GenrateApiKeyConcepts = () => ( + + + + + Flags live in projects + + Projects are containers for feature flags. When you create a + feature flag it will belong to the project you create it in. + + + + + + + + Flags have configuration in environments + + + In Unleash you can have multiple environments. Each feature + flag will have different configuration in every environment. + + + + + + + + SDKs connect to Unleash to retrieve configuration + + + 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. + + + + +); + +interface GeneratApiKeyProps { + project: string; + environments: string[]; +} + +export const GeneratApiKey = ({ + environments, + project, +}: GeneratApiKeyProps) => { + const [environment, setEnvironment] = useState(''); + + useEffect(() => { + if (environments.length > 0) { + setEnvironment(environments[0]); + } + }, [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 { setToastApiError } = useToast(); + + const generateAPIKey = async () => { + try { + await createToken( + { + environment, + type: 'CLIENT', + projects: [project], + username: `api-key-${project}-${environment}`, + }, + project, + ); + refreshTokens(); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + + return ( + + Connect an SDK to Unleash + + Environment + + The environment SDK will connect to in order to retrieve + configuration. + + {environments.length > 0 ? ( + + ) : null} + + + + API Key + {parsedToken ? ( + + Here is your generated API key. We will use it to + connect to the {parsedToken.project} project in + the {parsedToken.environment} environment. + + ) : ( + + 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. + + )} + {parsedToken ? ( + + ) : ( + + )} + + + ); +};