Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dashboard: fix and improve build #986

Merged
merged 11 commits into from
Aug 6, 2024
2 changes: 1 addition & 1 deletion packages/dashboard/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
Expand Down
2 changes: 1 addition & 1 deletion packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"build": "pnpm run --filter {.}^... build && vite build",
"build-storybook": "storybook build",
"lint": "tsc --build && eslint --max-warnings 0 src",
"start": "concurrently npm:start:rmf-server npm:start:react",
Expand Down
41 changes: 20 additions & 21 deletions packages/dashboard/src/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,7 @@ export interface KeycloakAuthConfig {
};
}

export interface AuthConfig {
provider: string;
}

export interface AppConfig {
rmfServerUrl: string;
trajectoryServerUrl: string;
auth: KeycloakAuthConfig | StubAuthConfig;
helpLink: string;
reportIssue: string;
Expand All @@ -58,32 +52,37 @@ export interface AppConfig {
defaultMapLevel: string;
allowedTasks: TaskResource[];
resources: Record<string, Resources> & Record<'default', Resources>;
customTabs: boolean;
adminTab: boolean;
// FIXME(koonpeng): this is used for very specific tasks, should be removed when mission
// system is implemented.
cartIds: string[];
}

export interface AppConfigJson extends AppConfig {
// these will be injected as global variables
rmfServerUrl: string;
trajectoryServerUrl: string;

// these will be injected as defines and potentially be tree shaken out
customTabs?: boolean;
adminTab?: boolean;
}

// we specifically don't export app config to force consumers to use the context.
const appConfig: AppConfig = appConfigJson as AppConfig;

export const AppConfigContext = React.createContext(appConfig);

const authenticator: Authenticator = (() => {
switch (appConfig.auth.provider) {
case 'keycloak':
if (!import.meta.env.VITE_KEYCLOAK_CONFIG) {
throw new Error('missing VITE_KEYCLOAK_CONFIG');
}
return new KeycloakAuthenticator(
appConfig.auth.config,
`${window.location.origin}${BasePath}/silent-check-sso.html`,
);
case 'stub':
return new StubAuthenticator();
default:
throw new Error('unknown auth provider');
// must use if statement instead of switch for vite tree shaking to work
if (APP_CONFIG_AUTH_PROVIDER === 'keycloak') {
return new KeycloakAuthenticator(
(appConfig.auth as KeycloakAuthConfig).config,
`${window.location.origin}${BasePath}/silent-check-sso.html`,
);
} else if (APP_CONFIG_AUTH_PROVIDER === 'stub') {
return new StubAuthenticator();
} else {
throw new Error('unknown auth provider');
}
})();

Expand Down
54 changes: 30 additions & 24 deletions packages/dashboard/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,32 +96,38 @@ export default function App(): JSX.Element | null {
}
/>

<Route
path={CustomRoute1}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<ManagedWorkspace key="custom1" workspaceId="custom1" />
</PrivateRoute>
}
/>
{APP_CONFIG_ENABLE_CUSTOM_TABS && (
<Route
path={CustomRoute1}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<ManagedWorkspace key="custom1" workspaceId="custom1" />
</PrivateRoute>
}
/>
)}

<Route
path={CustomRoute2}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<ManagedWorkspace key="custom2" workspaceId="custom2" />
</PrivateRoute>
}
/>
{APP_CONFIG_ENABLE_CUSTOM_TABS && (
<Route
path={CustomRoute2}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<ManagedWorkspace key="custom2" workspaceId="custom2" />
</PrivateRoute>
}
/>
)}

<Route
path={AdminRoute}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<AdminRouter />
</PrivateRoute>
}
/>
{APP_CONFIG_ENABLE_ADMIN_TAB && (
<Route
path={AdminRoute}
element={
<PrivateRoute unauthorizedComponent={loginRedirect} user={user}>
<AdminRouter />
</PrivateRoute>
}
/>
)}
</Routes>
</AppBase>
</RmfApp>
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard/src/components/appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea
aria-label="Tasks"
onTabClick={() => navigate(TasksRoute)}
/>
{appConfig.customTabs && (
{APP_CONFIG_ENABLE_CUSTOM_TABS && (
<>
<StyledAppBarTab
label="Custom 1"
Expand All @@ -360,7 +360,7 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea
/>
</>
)}
{appConfig.adminTab && profile?.user.is_admin && (
{APP_CONFIG_ENABLE_ADMIN_TAB && profile?.user.is_admin && (
<StyledAppBarTab
label="Admin"
value="admin"
Expand Down
11 changes: 4 additions & 7 deletions packages/dashboard/src/components/rmf-app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { AppConfigContext, AuthenticatorContext } from '../../app-config';
import { AuthenticatorContext } from '../../app-config';
import { UserProfileProvider } from '../../auth';
import { RmfIngress } from './rmf-ingress';

Expand All @@ -11,23 +11,20 @@ export const RmfAppContext = React.createContext<RmfIngress | undefined>(undefin
export interface RmfAppProps extends React.PropsWithChildren<{}> {}

export function RmfApp(props: RmfAppProps): JSX.Element {
const appConfig = React.useContext(AppConfigContext);
const authenticator = React.useContext(AuthenticatorContext);
const [rmfIngress, setRmfIngress] = React.useState<RmfIngress | undefined>(undefined);

React.useEffect(() => {
if (authenticator.user) {
return setRmfIngress(new RmfIngress(appConfig, authenticator));
return setRmfIngress(new RmfIngress(authenticator));
} else {
authenticator.once('userChanged', () =>
setRmfIngress(new RmfIngress(appConfig, authenticator)),
);
authenticator.once('userChanged', () => setRmfIngress(new RmfIngress(authenticator)));
return undefined;
}
}, [authenticator]);

return (
<UserProfileProvider authenticator={authenticator} basePath={appConfig.rmfServerUrl}>
<UserProfileProvider authenticator={authenticator} basePath={RMF_SERVER_URL}>
<RmfAppContext.Provider value={rmfIngress}>{props.children}</RmfAppContext.Provider>
</UserProfileProvider>
);
Expand Down
9 changes: 4 additions & 5 deletions packages/dashboard/src/components/rmf-app/rmf-ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
import axios from 'axios';
import { map, Observable, shareReplay } from 'rxjs';

import { AppConfig } from '../../app-config';
import { Authenticator } from '../../auth';
import { NegotiationStatusManager } from '../../managers/negotiation-status-manager';
import {
Expand Down Expand Up @@ -62,7 +61,7 @@ export class RmfIngress {
negotiationStatusManager: NegotiationStatusManager;
trajectoryManager: RobotTrajectoryManager;

constructor(appConfig: AppConfig, authenticator: Authenticator) {
constructor(authenticator: Authenticator) {
if (!authenticator.user) {
throw new Error(
'user is undefined, RmfIngress should only be initialized after the authenticator is ready',
Expand All @@ -71,7 +70,7 @@ export class RmfIngress {

this._sioClient = (() => {
const token = authenticator.token;
const url = new URL(appConfig.rmfServerUrl);
const url = new URL(RMF_SERVER_URL);
const path = url.pathname === '/' ? '' : url.pathname;

const options: ConstructorParameters<typeof SioClient>[1] = {
Expand Down Expand Up @@ -112,7 +111,7 @@ export class RmfIngress {
);
const apiConfig = new Configuration({
accessToken: authenticator.token,
basePath: appConfig.rmfServerUrl,
basePath: RMF_SERVER_URL,
});

this.beaconsApi = new BeaconsApi(apiConfig, undefined, axiosInst);
Expand All @@ -128,7 +127,7 @@ export class RmfIngress {
this.adminApi = new AdminApi(apiConfig, undefined, axiosInst);
this.deliveryAlertsApi = new DeliveryAlertsApi(apiConfig, undefined, axiosInst);

const ws = new WebSocket(appConfig.trajectoryServerUrl);
const ws = new WebSocket(TRAJECTORY_SERVER_URL);
this.trajectoryManager = new DefaultTrajectoryManager(ws, authenticator);
this.negotiationStatusManager = new NegotiationStatusManager(ws, authenticator);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/dashboard/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
/// <reference types="vite/client" />

declare const APP_CONFIG_AUTH_PROVIDER: string;
declare const APP_CONFIG_ENABLE_CUSTOM_TABS: boolean;
declare const APP_CONFIG_ENABLE_ADMIN_TAB: boolean;

declare const RMF_SERVER_URL: string;
declare const TRAJECTORY_SERVER_URL: string;
51 changes: 49 additions & 2 deletions packages/dashboard/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
/// <reference types="vitest" />

import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { defineConfig, Plugin } from 'vite';

import appConfig from './app-config.json';

/**
* The goal of this plugin is to inject global variables to `index.html`, allowing
* a crude way to configure some variables after the bundle is built via `sed`.
*
* An example use case is building a dockerfile, because the domain in a dev or staging
* environment is typically different from prod, we would normally end up needing to build
* different images (which is really detrimental for staging). In such scenario, you can
* set the `rmfServerUrl` to a placeholder like `__RMF_SERVER_URL_PLACEHOLDER__` in
* `app-config.json`, then do a search and replace at the container entrypoint.
*
* The reason for doing this outside the app code is to avoid these variables from
* being modified by the bundler, and to reduce the chance that the search and replace modify
* unintended code.
*/
const injectGlobals: Plugin = {
name: 'injectRuntimeArgs',
transformIndexHtml: {
order: 'pre', // must be injected before the app
handler: () => {
const globals = {
RMF_SERVER_URL: appConfig.rmfServerUrl,
TRAJECTORY_SERVER_URL: appConfig.trajectoryServerUrl,
};
return [
{
tag: 'script',
injectTo: 'head',
children: Object.entries(globals)
.map(([k, v]) => `const ${k}='${v}'`)
.join(';'),
},
];
},
},
};

function booleanToString(b: boolean | null | undefined) {
return b ? 'true' : 'false';
}

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
aaronchongth marked this conversation as resolved.
Show resolved Hide resolved
APP_CONFIG_AUTH_PROVIDER: `'${appConfig.auth.provider}'`,
APP_CONFIG_ENABLE_CUSTOM_TABS: `${booleanToString(appConfig.adminTab)}`,
koonpeng marked this conversation as resolved.
Show resolved Hide resolved
APP_CONFIG_ENABLE_ADMIN_TAB: `${booleanToString(appConfig.adminTab)}`,
},
plugins: [injectGlobals, react()],
test: {
environment: 'jsdom',
globals: true,
Expand Down
Loading