Skip to content
Merged
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
15 changes: 15 additions & 0 deletions contributing/FRONTEND.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,18 @@ The `webpack` dev server expects the API to be running on `http://127.0.0.1:8000
```shell
dstack server --port 8000
```

## Product flavors

The shared frontend supports OSS, Enterprise, Factory, and Sky. From `frontend/`, use
`npm run build`, `npm run build-enterprise`, `npm run build-factory`, or
`npm run build-sky`; the corresponding development commands are `start`,
`start-enterprise`, `start-factory`, and `start-sky`.

`frontend/product.config.cjs` defines product names and inherited UI features for both
webpack and the application. Components use `product` from `src/product.ts` instead of
checking `UI_VERSION` directly. Factory and Sky share billing and presets; hosted-service
content such as Sky terms and onboarding uses `product.isSky`.

All flavors use the same login component. It displays supported providers enabled by
`/api/auth/list_providers`, independently of the UI flavor, and always offers token login.
6 changes: 5 additions & 1 deletion frontend/jest.auth.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ module.exports = {
clearMocks: true,
testEnvironment: 'node',
moduleDirectories: ['node_modules', 'src'],
testMatch: ['<rootDir>/src/App/auth.test.tsx', '<rootDir>/src/services/preset.test.tsx'],
testMatch: [
'<rootDir>/src/App/auth.test.tsx',
'<rootDir>/src/App/login.test.tsx',
'<rootDir>/src/services/preset.test.tsx',
],
transform: {
'\\.[jt]sx?$': 'babel-jest',
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"start": "cross-env NODE_ENV=development webpack serve --config webpack.config.js",
"start-sky": "cross-env NODE_ENV=development UI_VERSION=sky webpack serve --config webpack.config.js",
"start-factory": "cross-env NODE_ENV=development UI_VERSION=factory webpack serve --config webpack.config.js",
"start-enterprise": "cross-env NODE_ENV=development UI_VERSION=enterprise webpack serve --config webpack.config.js",
"build": "cross-env NODE_ENV=production webpack build --config webpack.config.js",
"build-sky": "cross-env NODE_ENV=production UI_VERSION=sky webpack build --config webpack.config.js",
"build-factory": "cross-env NODE_ENV=production UI_VERSION=factory webpack build --config webpack.config.js",
"build-enterprise": "cross-env NODE_ENV=production UI_VERSION=enterprise webpack build --config webpack.config.js",
"eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"eslint-fix": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix",
"test": "jest",
Expand Down
27 changes: 27 additions & 0 deletions frontend/product.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* global module */
const oss = {
id: 'oss',
name: 'dstack',
hasEvents: false,
hasBilling: false,
hasPresets: false,
isSky: false,
};
const enterprise = { ...oss, id: 'enterprise', name: 'dstack Enterprise', hasEvents: true };
const factory = { ...enterprise, id: 'factory', name: 'dstack Factory', hasBilling: true, hasPresets: true };
const sky = { ...factory, id: 'sky', name: 'dstack Sky', isSky: true };

function getProductConfig(version) {
switch (version) {
case 'enterprise':
return enterprise;
case 'factory':
return factory;
case 'sky':
return sky;
default:
return oss;
}
}

module.exports = { getProductConfig };
79 changes: 13 additions & 66 deletions frontend/src/App/Login/LoginByGithub/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { colorBackgroundHomeHeader } from '@cloudscape-design/design-tokens';

import { Alert, Box, Button, Container, ContentLayout, Header, Link, NavigateLink, SpaceBetween } from 'components';
import { Alert, Button } from 'components';

import { useAppSelector } from 'hooks';
import { goToUrl } from 'libs';
import { ROUTES } from 'routes';
import { useGithubAuthorizeMutation } from 'services/auth';
import { useGetUserDataQuery } from 'services/user';

import { Loading } from 'App/Loading';
import { selectAuthToken } from 'App/slice';

const GitHubIcon: React.FC = () => (
<span>
Expand All @@ -25,71 +17,26 @@ const GitHubIcon: React.FC = () => (
);

export const LoginByGithub: React.FC = () => {
const token = useAppSelector(selectAuthToken);
const localStorageIsAvailable = 'localStorage' in window;
const {
currentData: userData,
error,
isFetching,
} = useGetUserDataQuery({ token }, { skip: !token || !localStorageIsAvailable });
const [githubAuthorize, { isLoading, isError }] = useGithubAuthorizeMutation();

const signInClick = () => {
githubAuthorize()
.unwrap()
.then((data) => goToUrl(data.authorization_url))
.catch(() => undefined);
};

if (token && localStorageIsAvailable && isFetching && !userData) return <Loading />;
if (token && localStorageIsAvailable && userData?.username && !error) {
return <Navigate replace to={ROUTES.RUNS.LIST} />;
}

return (
<ContentLayout
defaultPadding
headerVariant="high-contrast"
maxContentWidth={500}
headerBackgroundStyle={colorBackgroundHomeHeader}
header={
<Box variant="h1" padding={{ vertical: 'xxxl' }} textAlign="center">
Welcome to dstack Sky
</Box>
}
>
<Container
header={
<Box padding={{ bottom: 'xs' }}>
<Header variant="h2">Sign in</Header>
</Box>
}
<>
{isError && <Alert type="error">Unable to start GitHub authentication</Alert>}
<Button
fullWidth
onClick={signInClick}
disabled={isLoading}
loading={isLoading}
variant="primary"
iconSvg={<GitHubIcon />}
>
<SpaceBetween size="l">
{isError && <Alert type="error">Unable to start GitHub authentication</Alert>}
<Button
fullWidth
onClick={signInClick}
disabled={isLoading}
loading={isLoading}
variant="primary"
iconSvg={<GitHubIcon />}
>
Continue with GitHub
</Button>
<Box color="text-body-secondary" fontSize="body-s">
By continuing with GitHub, you agree to the{' '}
<Link href="https://dstack.ai/terms/" target="_blank" external>
Terms
</Link>{' '}
and{' '}
<Link href="https://dstack.ai/privacy/" target="_blank" external>
Privacy policy
</Link>
</Box>
<NavigateLink href={ROUTES.AUTH.TOKEN}>Sign in with a token</NavigateLink>
</SpaceBetween>
</Container>
</ContentLayout>
Continue with GitHub
</Button>
</>
);
};
3 changes: 2 additions & 1 deletion frontend/src/App/Login/LoginByGithubCallback/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { product } from 'product';

import { NavigateLink } from 'components';
import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout';
Expand Down Expand Up @@ -41,7 +42,7 @@ export const LoginByGithubCallback: React.FC = () => {
.unwrap()
.then(async ({ creds: { token } }) => {
dispatch(setAuthData({ token }));
if (process.env.UI_VERSION === 'sky') {
if (product.hasPresets) {
const result = await getProjects({}).unwrap();
if (result.data.length === 0) {
navigate(ROUTES.PROJECT.ADD);
Expand Down
65 changes: 0 additions & 65 deletions frontend/src/App/Login/SelfHostedLogin/index.tsx

This file was deleted.

44 changes: 2 additions & 42 deletions frontend/src/App/Login/TokenLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { colorBackgroundHomeHeader } from '@cloudscape-design/design-tokens';

import { Box, Container, ContentLayout, Header, NavigateLink, SpaceBetween } from 'components';
import { Login } from '../index';

import { ROUTES } from 'routes';

import { LoginByTokenForm } from '../LoginByTokenForm';
import { SelfHostedLogin } from '../SelfHostedLogin';

export const TokenLogin: React.FC = () => {
const { t } = useTranslation();

if (process.env.UI_VERSION === 'sky') {
return (
<ContentLayout
defaultPadding
headerVariant="high-contrast"
maxContentWidth={500}
headerBackgroundStyle={colorBackgroundHomeHeader}
header={
<Box variant="h1" padding={{ vertical: 'xxxl' }} textAlign="center">
{t('auth.sign_in_to_dstack_sky')}
</Box>
}
>
<Container
header={
<Box padding={{ bottom: 'xs' }}>
<Header variant="h2">Sign in with a token</Header>
</Box>
}
>
<SpaceBetween size="l">
<LoginByTokenForm />
<NavigateLink href={ROUTES.BASE}>{t('auth.another_login_methods')}</NavigateLink>
</SpaceBetween>
</Container>
</ContentLayout>
);
}

return <SelfHostedLogin tokenOnly />;
};
export const TokenLogin: React.FC = () => <Login tokenOnly />;
Loading
Loading