diff --git a/frontend/src/component/onboarding/ConnectSdkDialog.tsx b/frontend/src/component/onboarding/ConnectSdkDialog.tsx index d22822edbf65..d57ade6879d4 100644 --- a/frontend/src/component/onboarding/ConnectSdkDialog.tsx +++ b/frontend/src/component/onboarding/ConnectSdkDialog.tsx @@ -8,8 +8,15 @@ import { } from '@mui/material'; import { GenerateApiKey } from './GenerateApiKey'; import { useEffect, useState } from 'react'; -import { type Sdk, SelectSdk } from './SelectSdk'; -import { GenrateApiKeyConcepts, SelectSdkConcepts } from './UnleashConcepts'; +import { SelectSdk } from './SelectSdk'; +import { + ConceptsDefinitionsWrapper, + GenrateApiKeyConcepts, + SelectSdkConcepts, +} from './UnleashConcepts'; +import { TestSdkConnection } from './TestSdkConnection'; + +import type { Sdk } from './sharedTypes'; interface IConnectSDKDialogProps { open: boolean; @@ -107,7 +114,9 @@ export const ConnectSdkDialog = ({ }} /> ) : null} - {isTestConnectionStage ?
Last stage
: null} + {isTestConnectionStage ? ( + + ) : null} {stage === 'generate-api-key' ? ( @@ -163,6 +172,9 @@ export const ConnectSdkDialog = ({ {isLargeScreen && isGenerateApiKeyStage ? ( ) : null} + {isLargeScreen && isTestConnectionStage ? ( + + ) : null} ); diff --git a/frontend/src/component/onboarding/GenerateApiKey.tsx b/frontend/src/component/onboarding/GenerateApiKey.tsx index f11c87fdb2d6..8fdf0db3f677 100644 --- a/frontend/src/component/onboarding/GenerateApiKey.tsx +++ b/frontend/src/component/onboarding/GenerateApiKey.tsx @@ -1,8 +1,8 @@ -import { useProjectApiTokens } from '../../hooks/api/getters/useProjectApiTokens/useProjectApiTokens'; -import useProjectApiTokensApi from '../../hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi'; +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 useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; import { Box, Button, @@ -15,6 +15,7 @@ import { SingleSelectConfigButton } from '../common/DialogFormTemplate/ConfigBut import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; import { ArcherContainer, ArcherElement } from 'react-archer'; import { useEffect } from 'react'; +import { SectionHeader } from './SharedComponents'; const ChooseEnvironment = ({ environments, @@ -79,12 +80,6 @@ const TokenExplanationBox = styled(Box)(({ theme }) => ({ flexWrap: 'wrap', })); -const SectionHeader = styled('div')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, - marginBottom: theme.spacing(1), - fontSize: theme.typography.body1.fontSize, -})); - const SectionDescription = styled('p')(({ theme }) => ({ color: theme.palette.text.secondary, fontSize: theme.typography.body2.fontSize, diff --git a/frontend/src/component/onboarding/SelectSdk.tsx b/frontend/src/component/onboarding/SelectSdk.tsx index 84b77cf5517e..00354899d4d8 100644 --- a/frontend/src/component/onboarding/SelectSdk.tsx +++ b/frontend/src/component/onboarding/SelectSdk.tsx @@ -16,6 +16,8 @@ import rust from 'assets/icons/sdks/Logo-rust.svg'; import svelte from 'assets/icons/sdks/Logo-svelte.svg'; import vue from 'assets/icons/sdks/Logo-vue.svg'; import { formatAssetPath } from 'utils/formatPath'; +import { SectionHeader } from './SharedComponents'; +import type { ClientSdkName, Sdk, ServerSdkName } from './sharedTypes'; const SpacedContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(5, 8, 8, 8), @@ -24,12 +26,6 @@ const SpacedContainer = styled('div')(({ theme }) => ({ gap: theme.spacing(3), })); -const PrimarySectionHeader = styled('div')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, - marginBottom: theme.spacing(1), - fontSize: theme.typography.body1.fontSize, -})); - const SecondarySectionHeader = styled('div')(({ theme }) => ({ marginTop: theme.spacing(4), marginBottom: theme.spacing(2), @@ -71,7 +67,7 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({ boxShadow: theme.shadows[2], })); -const serverSdks = [ +const serverSdks: { name: ServerSdkName; icon: string }[] = [ { name: 'Node', icon: node }, { name: 'Golang', icon: go }, { name: 'Ruby', icon: ruby }, @@ -82,7 +78,7 @@ const serverSdks = [ { name: 'Python', icon: python }, ]; -const clientSdks = [ +const clientSdks: { name: ClientSdkName; icon: string }[] = [ { name: 'Javascript', icon: javascript }, { name: 'React', icon: react }, { name: 'Vue', icon: vue }, @@ -92,8 +88,6 @@ const clientSdks = [ { name: 'Flutter', icon: flutter }, ]; -type SdkType = 'client' | 'frontend'; -export type Sdk = { name: string; type: SdkType }; interface ISelectSdkProps { onSelect: (sdk: Sdk) => void; } @@ -102,7 +96,7 @@ export const SelectSdk: FC = ({ onSelect }) => { Connect an SDK to Unleash - Select SDK + Select SDK Server side SDKs @@ -155,5 +149,3 @@ export const SelectSdk: FC = ({ onSelect }) => { ); }; - -export const SelectSdkConcepts = () => {}; diff --git a/frontend/src/component/onboarding/SharedComponents.tsx b/frontend/src/component/onboarding/SharedComponents.tsx new file mode 100644 index 000000000000..181f8ebf6355 --- /dev/null +++ b/frontend/src/component/onboarding/SharedComponents.tsx @@ -0,0 +1,7 @@ +import { styled } from '@mui/material'; + +export const SectionHeader = styled('div')(({ theme }) => ({ + fontWeight: theme.typography.fontWeightBold, + marginBottom: theme.spacing(1), + fontSize: theme.typography.body1.fontSize, +})); diff --git a/frontend/src/component/onboarding/TestSdkConnection.tsx b/frontend/src/component/onboarding/TestSdkConnection.tsx new file mode 100644 index 000000000000..3e3f6d00f8d4 --- /dev/null +++ b/frontend/src/component/onboarding/TestSdkConnection.tsx @@ -0,0 +1,60 @@ +import type { FC } from 'react'; +import { Box, styled, Typography } from '@mui/material'; +import { SectionHeader } from './SharedComponents'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import type { Sdk } from './sharedTypes'; +import { codeSnippets, installCommands } from './sdkSnippets'; + +const SpacedContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(5, 8, 8, 8), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), +})); + +const StyledCodeBlock = styled('pre')(({ theme }) => ({ + backgroundColor: theme.palette.background.elevation1, + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + overflow: 'auto', + fontSize: theme.typography.body2.fontSize, + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', +})); + +interface ITestSdkConnectionProps { + sdk: Sdk; + apiKey: string; +} +export const TestSdkConnection: FC = ({ + sdk, + apiKey, +}) => { + const { uiConfig } = useUiConfig(); + + const clientApiUrl = `${uiConfig.unleashUrl}/api/`; + const frontendApiUrl = `${uiConfig.unleashUrl}/api/frontend/`; + const apiUrl = sdk.type === 'client' ? clientApiUrl : frontendApiUrl; + const codeSnippet = + codeSnippets[sdk.name] || `No snippet found for the ${sdk.name} SDK`; + const installCommand = + installCommands[sdk.name] || + `No install command found for the ${sdk.name} SDK`; + + return ( + + Connect an SDK to Unleash + + Setup the SDK +

1. Install the SDK

+ {installCommand} +

2. Initialize the SDK

+ + {codeSnippet + .replace('', apiKey) + .replace('', apiUrl)} + +
+
+ ); +}; diff --git a/frontend/src/component/onboarding/UnleashConcepts.tsx b/frontend/src/component/onboarding/UnleashConcepts.tsx index c62e1ebe49f6..a91804b1ba6d 100644 --- a/frontend/src/component/onboarding/UnleashConcepts.tsx +++ b/frontend/src/component/onboarding/UnleashConcepts.tsx @@ -3,7 +3,7 @@ import { ProjectIcon } from '../common/ProjectIcon/ProjectIcon'; import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; import CodeIcon from '@mui/icons-material/Code'; -const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({ +export const ConceptsDefinitionsWrapper = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.background.sidebar, padding: theme.spacing(12, 6, 6, 6), flex: 0, diff --git a/frontend/src/component/onboarding/sdkSnippets.ts b/frontend/src/component/onboarding/sdkSnippets.ts new file mode 100644 index 000000000000..46960bfd665c --- /dev/null +++ b/frontend/src/component/onboarding/sdkSnippets.ts @@ -0,0 +1,183 @@ +import type { SdkName } from './sharedTypes'; + +export const installCommands: Record = { + Node: ' npm install unleash-client', + Golang: 'go get github.com/Unleash/unleash-client-go/v3', + Ruby: 'gem install unleash', + PHP: 'composer require unleash/client', + Rust: 'cargo add unleash-client', + DotNet: `dotnet add package unleash.client +// If you do not have a json library in your project: +dotnet add package Newtonsoft.Json`, + Java: ` + io.getunleash + unleash-client-java + Latest version here +`, + Python: 'pip install UnleashClient', + Javascript: 'npm install unleash-proxy-client', + React: 'npm install @unleash/proxy-client-react unleash-proxy-client', + Vue: 'npm install @unleash/proxy-client-vue', + Svelte: 'npm install @unleash/proxy-client-svelte', + Swift: 'https://github.com/Unleash/unleash-proxy-client-swift', + Android: + 'implementation("io.getunleash:unleash-android:${unleash.sdk.version}")', + Flutter: 'flutter pub add unleash_proxy_client_flutter', +}; + +export const codeSnippets: Record = { + Node: `import { initialize } from 'unleash-client'; + +const unleash = initialize({ + url: '', + appName: 'unleash-onboarding-node', + customHeaders: { Authorization: '' }, +}); +`, + Golang: `import ( + "github.com/Unleash/unleash-client-go/v3" +) + +func init() { + unleash.Initialize( + unleash.WithListener(&unleash.DebugListener{}), + unleash.WithAppName("unleash-onboarding-golang"), + unleash.WithUrl(""), + unleash.WithCustomHeaders(http.Header{"Authorization": {""}}), + ) +}`, + Ruby: `Unleash.configure do |config| + config.app_name = 'unleash-onboarding-ruby' + config.url = '' + config.custom_http_headers = {'Authorization': ''} +end`, + PHP: `withAppName('unleash-onboarding-php') + ->withAppUrl('') + ->withHeader('Authorization', '') + ->withInstanceId('unleash-onboarding-instance') + ->build();`, + Rust: `let client = client::ClientBuilder::default() + .interval(500) + .into_client::( + "", + "unleash-onboarding-rust", + "unleash-onboarding-instance", + "", + )?; +client.register().await?;`, + DotNet: `using Unleash; +var settings = new UnleashSettings() +{ + AppName = "unleash-onboarding-dotnet", + UnleashApi = new Uri(""), + CustomHttpHeaders = new Dictionary() + { + {"Authorization","" } + } +};`, + Java: `UnleashConfig config = UnleashConfig.builder() + .appName("unleash-onboarding-java") + .instanceId("unleash-onboarding-instance") + .unleashAPI("") + .apiKey("") + .build(); + +Unleash unleash = new DefaultUnleash(config);`, + Python: `from UnleashClient import UnleashClient + +client = UnleashClient( + url="", + app_name="unleash-onboarding-python", + custom_headers={'Authorization': '"'}) + +client.initialize_client()`, + Javascript: `import { UnleashClient } from 'unleash-proxy-client'; + +const unleash = new UnleashClient({ + url: '', + clientKey: '', + appName: 'unleash-onboarding-javascript', +}); + +// Start the background polling +unleash.start();`, + React: `import { createRoot } from 'react-dom/client'; +import { FlagProvider } from '@unleash/proxy-client-react'; + +const config = { + url: '', + clientKey: '', + refreshInterval: 15, + appName: 'unleash-onboarding-react', +}; + +const root = createRoot(document.getElementById('root')); + +root.render( + + + + + +);`, + Vue: `import { createApp } from 'vue' +import { plugin as unleashPlugin } from '@unleash/proxy-client-vue' +// import the root component App from a single-file component. +import App from './App.vue' + +const config = { + url: ''', + clientKey: '', + refreshInterval: 15, + appName: 'unleash-onboarding-vue', +} + +const app = createApp(App) +app.use(unleashPlugin, { config }) +app.mount('#app')`, + Svelte: ` + + + +`, + Swift: `import SwiftUI +import UnleashProxyClientSwift + +var unleash = UnleashProxyClientSwift.UnleashClient( + unleashUrl: "", + clientKey: "", + refreshInterval: 15, + appName: "unleash-onboarding-swift", + context: [:]) + +unleash.start()`, + Android: `val unleash = DefaultUnleash( + androidContext = applicationContext, // likely a reference to your Android application context + unleashConfig = UnleashConfig.newBuilder(appName = "unleash-onboarding-android") + .proxyUrl("") + .clientKey("") + .pollingStrategy.interval(3000) + .metricsStrategy.interval(3000) + .build() +)`, + Flutter: `import 'package:unleash_proxy_client_flutter/unleash_proxy_client_flutter.dart'; + +final unleash = UnleashClient( + url: Uri.parse(''), + clientKey: '', + appName: 'unleash-onboarding-flutter');`, +}; diff --git a/frontend/src/component/onboarding/sharedTypes.ts b/frontend/src/component/onboarding/sharedTypes.ts new file mode 100644 index 000000000000..c1c5117bed68 --- /dev/null +++ b/frontend/src/component/onboarding/sharedTypes.ts @@ -0,0 +1,20 @@ +export type SdkType = 'client' | 'frontend'; +export type Sdk = { name: SdkName; type: SdkType }; +export type ServerSdkName = + | 'Node' + | 'Golang' + | 'Ruby' + | 'PHP' + | 'Rust' + | 'DotNet' + | 'Java' + | 'Python'; +export type ClientSdkName = + | 'Javascript' + | 'React' + | 'Vue' + | 'Svelte' + | 'Swift' + | 'Android' + | 'Flutter'; +export type SdkName = ServerSdkName | ClientSdkName;