diff --git a/frontend/src/component/onboarding/ConnectSDKDialog.tsx b/frontend/src/component/onboarding/ConnectSDKDialog.tsx
index eaad1c0e9d..e5d597173a 100644
--- a/frontend/src/component/onboarding/ConnectSDKDialog.tsx
+++ b/frontend/src/component/onboarding/ConnectSDKDialog.tsx
@@ -5,12 +5,20 @@ import {
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';
interface IConnectSDKDialogProps {
open: boolean;
@@ -19,10 +27,11 @@ interface IConnectSDKDialogProps {
environments: string[];
}
-const ConceptsDefinitions = styled('div')(({ theme }) => ({
+const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.sidebar,
- padding: theme.spacing(8),
- flexBasis: '30%',
+ padding: theme.spacing(6),
+ flex: 0,
+ minWidth: '400px',
}));
const IconStyle = ({ theme }: { theme: Theme }) => ({
@@ -59,7 +68,7 @@ export const APIKeyGeneration = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
- flexBasis: '70%',
+ flex: 1,
}));
export const SpacedContainer = styled('div')(({ theme }) => ({
@@ -106,15 +115,198 @@ const NextStepSectionSpacedContainer = styled('div')(({ theme }) => ({
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 = () => (
+
+
+
+
+ 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('');
- const longestEnv = Math.max(
- ...environments.map((environment) => environment.length),
- );
useEffect(() => {
if (environments.length > 0) {
@@ -122,6 +314,34 @@ export const ConnectSDKDialog = ({
}
}, [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 (
@@ -130,49 +350,54 @@ export const ConnectSDKDialog = ({
Connect an SDK to Unleash
-
+
Environment
The environment SDK will connect to in order to
retrieve configuration.
{environments.length > 0 ? (
- ({
- label: environment,
- value: environment,
- }),
- )}
- onChange={(value: any) => {
- setEnvironment(value);
- }}
- button={{
- label: environment,
- icon: ,
- labelWidth: `${longestEnv + 5}ch`,
- }}
- search={{
- label: 'Filter project mode options',
- placeholder: 'Select project mode',
- }}
+
) : null}
API Key
-
- 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 ? (
+
+ 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 ? (
+
+ ) : (
+
+ )}
@@ -185,49 +410,8 @@ export const ConnectSDKDialog = ({
-
-
-
-
-
- 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.
-
-
-
-
+
+ {isLargeScreen ? : null}
);
diff --git a/frontend/src/component/onboarding/parseToken.test.ts b/frontend/src/component/onboarding/parseToken.test.ts
new file mode 100644
index 0000000000..84e89fe67f
--- /dev/null
+++ b/frontend/src/component/onboarding/parseToken.test.ts
@@ -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();
+ });
+});
diff --git a/frontend/src/component/onboarding/parseToken.ts b/frontend/src/component/onboarding/parseToken.ts
new file mode 100644
index 0000000000..a3aa7846f3
--- /dev/null
+++ b/frontend/src/component/onboarding/parseToken.ts
@@ -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;
+};
diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts
index 76037f524a..c1ec1b52f7 100644
--- a/frontend/src/themes/theme.ts
+++ b/frontend/src/themes/theme.ts
@@ -94,7 +94,7 @@ export const theme = {
contrastText: colors.grey[50], // Color used for content when primary.main is used as a background
},
secondary: {
- // Used for purple badges and puple light elements
+ // Used for purple badges and purple light elements
main: colors.purple[800],
light: colors.purple[50],
dark: colors.purple[900], // Color used for text
@@ -150,7 +150,7 @@ export const theme = {
default: colors.grey[50],
application: colors.grey[300],
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],
elevation2: colors.grey[200],
},