Skip to content

Commit

Permalink
Introduce RestateContext and extract health check and version to se…
Browse files Browse the repository at this point in the history
…parate libs (#77)

Signed-off-by: Nik Nasr <[email protected]>
  • Loading branch information
nikrooz authored Oct 21, 2024
1 parent 8727ea7 commit 6175a1f
Show file tree
Hide file tree
Showing 43 changed files with 727 additions and 86 deletions.
89 changes: 16 additions & 73 deletions apps/web-ui/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { RouterProvider } from 'react-aria-components';
import { Button, Spinner } from '@restate/ui/button';
import { useCallback } from 'react';
import { QueryProvider } from '@restate/util/react-query';
import {
AdminBaseURLProvider,
useVersion,
} from '@restate/data-access/admin-api';
import { Nav, NavItem } from '@restate/ui/nav';
import { Icon, IconName } from '@restate/ui/icons';
import { tv } from 'tailwind-variants';
import { RestateContextProvider } from '@restate/features/restate-context';
import { Version } from '@restate/features/version';
import {
HealthCheckNotification,
HealthIndicator,
} from '@restate/features/health';

export const links: LinksFunction = () => [
{
Expand Down Expand Up @@ -79,97 +80,39 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}

const miniStyles = tv({
base: '',
slots: {
container: 'relative w-3 h-3 text-xs',
icon: 'absolute left-0 top-[1px] w-3 h-3 stroke-0 fill-current',
animation:
'absolute inset-left-0 top-[1px] w-3 h-3 stroke-[4px] fill-current opacity-20',
},
variants: {
status: {
PENDING: {
container: 'text-yellow-500',
animation: 'animate-ping',
},
DEGRADED: {
container: 'text-yellow-500',
animation: 'animate-ping',
},
ACTIVE: { container: 'text-green-500', animation: 'animate-ping' },
HEALTHY: { container: 'text-green-500', animation: 'animate-ping' },
FAILED: { container: 'text-red-500', animation: 'animate-ping' },
DELETED: { container: 'text-gray-400', animation: 'hidden' },
},
},
});
// TODO
function Version() {
const { data } = useVersion();

if (!data?.version) {
return null;
}

return (
<span className="text-2xs font-mono items-center rounded-xl px-2 leading-4 bg-white/50 ring-1 ring-inset ring-gray-500/20 text-gray-500 mt-0.5">
v{data?.version}
</span>
);
}

function getCookieValue(name: string) {
const cookies = document.cookie
.split(';')
.map((cookie) => cookie.trim().split('='));
const cookieValue = cookies.find(([key]) => key === name)?.at(1);
return cookieValue ? decodeURIComponent(cookieValue) : null;
return cookieValue && decodeURIComponent(cookieValue);
}

export default function App() {
const { container, icon, animation } = miniStyles();

return (
<AdminBaseURLProvider baseUrl={getCookieValue('adminBaseUrl') ?? ''}>
<QueryProvider>
<QueryProvider>
<RestateContextProvider adminBaseUrl={getCookieValue('adminBaseUrl')}>
<LayoutOutlet zone={LayoutZone.Content}>
<Outlet />
</LayoutOutlet>
<LayoutOutlet zone={LayoutZone.AppBar}>
<div className="flex items-center gap-2 flex-1">
<div className="flex gap-2 items-center rounded-xl border bg-white px-3 shadow-sm h-full">
<div className="flex items-stretch gap-2 flex-1">
<div className="flex gap-2 items-center rounded-xl border bg-white p-3 shadow-sm h-full">
<Icon
name={IconName.RestateEnvironment}
className="text-xl text-[#222452]"
/>
</div>
<Button
variant="secondary"
className="flex items-center gap-2 px-2 py-1 bg-transparent border-none shadow-none"
className="flex items-center gap-2 px-2 my-1 bg-transparent border-none shadow-none"
>
<div
className={container({ status: 'HEALTHY' })}
role="status"
aria-label={'HEALTHY'}
>
<Icon
name={IconName.Circle}
className={icon({ status: 'HEALTHY' })}
/>
<Icon
name={IconName.Circle}
className={animation({ status: 'HEALTHY' })}
/>
</div>
<div className="truncate row-start-1 col-start-2 w-full flex items-center gap-2">
<HealthIndicator mini className="-mt-0.5" />
<HealthCheckNotification />
<span className="flex-auto truncate">Restate server</span>
<Version />
</div>
<Icon
name={IconName.ChevronsUpDown}
className="text-gray-400 flex-shrink-0"
/>
</Button>
<LayoutOutlet zone={LayoutZone.Nav}>
<Nav ariaCurrentValue="page">
Expand All @@ -179,8 +122,8 @@ export default function App() {
</LayoutOutlet>
</div>
</LayoutOutlet>
</QueryProvider>
</AdminBaseURLProvider>
</RestateContextProvider>
</QueryProvider>
);
}

Expand Down
28 changes: 27 additions & 1 deletion apps/web-ui/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,33 @@
"start": {
"executor": "nx:run-commands",
"options": {
"command": "cd apps/web-ui && ../../node_modules/.bin/remix vite:build && ../../node_modules/.bin/vite preview --port=4300"
"commands": [
"cd apps/web-ui && ../../node_modules/.bin/remix vite:build && ../../node_modules/.bin/vite preview --port=4300"
]
},
"configurations": {
"mock": {
"commands": [
{
"command": "nx serve mock-admin-api"
},
{
"command": "cd apps/web-ui && ../../node_modules/.bin/remix vite:build && ADMIN_BASE_URL=http://localhost:4001 ../../node_modules/.bin/vite preview --port=4300"
}
],
"parallel": true
},
"local": {
"commands": [
{
"command": "nx serve mock-admin-api -c proxy"
},
{
"command": "cd apps/web-ui && ../../node_modules/.bin/remix vite:build && ADMIN_BASE_URL=http://localhost:4001 ../../node_modules/.bin/vite preview --port=4300"
}
],
"parallel": true
}
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions apps/web-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { defineConfig, loadEnv } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

const BASE_URL = '/ui/';
const ADMIN_BASE_URL = process.env['ADMIN_BASE_URL'] || '';
const SERVER_HEADERS = {
'Set-Cookie': `adminBaseUrl=${ADMIN_BASE_URL}; SameSite=Strict; Path=/`,
};

export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const ADMIN_BASE_URL = env['ADMIN_BASE_URL'] || '';
const SERVER_HEADERS = {
'Set-Cookie': `adminBaseUrl=${ADMIN_BASE_URL}; SameSite=Strict; Path=/`,
};

return {
base: BASE_URL,
Expand Down
2 changes: 1 addition & 1 deletion libs/data-access/admin-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './lib/api/client';
export * from './lib/AdminBaseUrlProvider';
export * from './lib/api/hooks';
export type * from './lib/api/type';
export * from './lib/AdminBaseUrlProvider';
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export function AdminBaseURLProvider({

export function useAdminBaseUrl() {
const { baseUrl } = useContext(AdminBaseURLContext);
return baseUrl;
return baseUrl ?? '';
}
6 changes: 6 additions & 0 deletions libs/data-access/admin-api/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function adminApi<
): {
queryFn: QueryFn<Path, Method>;
queryKey: QueryKey<Path, Method>;
meta: Record<string, unknown>;
};
export function adminApi<
Path extends keyof paths,
Expand All @@ -169,6 +170,7 @@ export function adminApi<
): {
mutationFn: MutationFn<Path, Method, Parameters, Body>;
mutationKey: MutationKey<Path, Method, Parameters, Body>;
meta: Record<string, unknown>;
};
export function adminApi<
Path extends keyof paths,
Expand All @@ -188,16 +190,19 @@ export function adminApi<
| {
queryFn: QueryFn<Path, Method>;
queryKey: QueryKey<Path, Method>;
meta: Record<string, unknown>;
}
| {
mutationFn: MutationFn<Path, Method, Parameters, Body>;
mutationKey: MutationKey<Path, Method, Parameters, Body>;
meta: Record<string, unknown>;
} {
const key = [path, { ...init, method }];

if (type === 'query') {
return {
queryKey: key,
meta: { path, method, isAdmin: true },
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const { data } = await (client as any)[String(method).toUpperCase()](
path,
Expand All @@ -218,6 +223,7 @@ export function adminApi<
} else {
return {
mutationKey: key,
meta: { path, method, isAdmin: true },
mutationFn: async (variables: {
parameters?: Parameters;
body?: Body;
Expand Down
2 changes: 1 addition & 1 deletion libs/data-access/admin-api/src/lib/api/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { paths } from './index'; // generated by openapi-typescript
import { useMutation, useQuery } from '@tanstack/react-query';
import { useAdminBaseUrl } from '../AdminBaseUrlProvider';
import {
adminApi,
MutationOptions,
Expand All @@ -9,6 +8,7 @@ import {
QueryOptions,
SupportedMethods,
} from './client';
import { useAdminBaseUrl } from '../AdminBaseUrlProvider';

type HookQueryOptions<
Path extends keyof paths,
Expand Down
12 changes: 12 additions & 0 deletions libs/features/health/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
7 changes: 7 additions & 0 deletions libs/features/health/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# health

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test health` to execute the unit tests via [Vitest](https://vitest.dev/).
12 changes: 12 additions & 0 deletions libs/features/health/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');

module.exports = [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
9 changes: 9 additions & 0 deletions libs/features/health/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "health",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/features/health/src",
"projectType": "library",
"tags": [],
"// targets": "to see all targets run: nx show project health --web",
"targets": {}
}
2 changes: 2 additions & 0 deletions libs/features/health/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/HealthIndicator';
export * from './lib/HealthCheckNotification';
42 changes: 42 additions & 0 deletions libs/features/health/src/lib/HealthCheckNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRestateContext } from '@restate/features/restate-context';
import { Button } from '@restate/ui/button';
import { Icon, IconName } from '@restate/ui/icons';
import { HideNotification, LayoutOutlet, LayoutZone } from '@restate/ui/layout';
import { useDeferredValue, useState } from 'react';

export function HealthCheckNotification() {
const { status } = useRestateContext();
const isDegraded = status === 'DEGRADED';
const deferredIsDegraded = useDeferredValue(isDegraded);
const [canBeOpened, setCanBeOpened] = useState(true);
const deferredCanBeOpened = useDeferredValue(canBeOpened);

if (deferredIsDegraded && deferredCanBeOpened) {
return (
<LayoutOutlet zone={LayoutZone.Notification}>
<div className="flex items-center gap-2 bg-orange-100 rounded-xl bg-orange-200/60 shadow-lg shadow-zinc-800/5 border border-orange-200 text-orange-800 px-3 text-sm">
<Icon
name={IconName.TriangleAlert}
className="w-4 h-4 fill-current2"
/>{' '}
Your Restate server is currently experiencing issues.
<Button
variant="icon"
className="ml-auto"
onClick={(event) => {
event.target.dataset.variant = 'hidden';
setTimeout(() => {
setCanBeOpened(false);
}, 100);
}}
>
<Icon name={IconName.X} />
</Button>
</div>
{(!isDegraded || !canBeOpened) && <HideNotification />}
</LayoutOutlet>
);
}

return null;
}
Loading

0 comments on commit 6175a1f

Please sign in to comment.