Browse Source

feat(dashboard): Add action bar alerts (#3493)

Michael Bromley 9 months ago
parent
commit
e5845dc024

File diff suppressed because it is too large
+ 684 - 100
package-lock.json


+ 19 - 1
packages/dashboard/src/lib/components/shared/alerts.tsx

@@ -1,19 +1,37 @@
 import { BellIcon } from 'lucide-react';
 import { Button } from '../ui/button.js';
 import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog.js';
+import { useAlerts } from '../../framework/alert/alert-extensions.js';
+import { AlertItem } from '../../framework/alert/alert-item.js';
+import { ScrollArea } from '../ui/scroll-area.js';
+import { AlertsIndicator } from '../../framework/alert/alerts-indicator.js';
 
 export function Alerts() {
+    const { alerts } = useAlerts();
+
+    if (alerts.length === 0) {
+        return null;
+    }
+
     return (
         <Dialog>
             <DialogTrigger asChild>
-                <Button size="icon" variant="ghost">
+                <Button size="icon" variant="ghost" className="relative">
                     <BellIcon />
+                    <AlertsIndicator />
                 </Button>
             </DialogTrigger>
             <DialogContent>
                 <DialogHeader>
                     <DialogTitle>Alerts</DialogTitle>
                 </DialogHeader>
+                <ScrollArea className="max-h-[500px]">
+                    <div className="flex flex-col divide-y divide-border">
+                        {alerts.map(alert => (
+                            <AlertItem className="py-2" key={alert.id} alert={alert} />
+                        ))}
+                    </div>
+                </ScrollArea>
             </DialogContent>
         </Dialog>
     );

+ 31 - 0
packages/dashboard/src/lib/framework/alert/alert-extensions.tsx

@@ -0,0 +1,31 @@
+import { useEffect } from 'react';
+import { useState } from 'react';
+import { globalRegistry } from '../registry/global-registry.js';
+import { DashboardAlertDefinition } from './types.js';
+
+globalRegistry.register('dashboardAlertRegistry', new Map<string, DashboardAlertDefinition>());
+
+export function registerAlert<TResponse>(alert: DashboardAlertDefinition<TResponse>) {
+    globalRegistry.set('dashboardAlertRegistry', map => {
+        map.set(alert.id, alert);
+        return map;
+    });
+}
+
+export function getAlertRegistry() {
+    return globalRegistry.get('dashboardAlertRegistry');
+}
+
+export function getAlert(id: string) {
+    return getAlertRegistry().get(id);
+}
+
+export function useAlerts() {
+    const [alerts, setAlerts] = useState<DashboardAlertDefinition[]>([]);
+
+    useEffect(() => {
+        setAlerts(Array.from(getAlertRegistry().values()));
+    }, []);
+
+    return { alerts };
+}

+ 47 - 0
packages/dashboard/src/lib/framework/alert/alert-item.tsx

@@ -0,0 +1,47 @@
+import { Button } from '@/components/ui/button.js';
+import { useQuery } from '@tanstack/react-query';
+import { ComponentProps } from 'react';
+import { DashboardAlertDefinition } from './types.js';
+import { cn } from '@/lib/utils.js';
+interface AlertItemProps extends ComponentProps<'div'> {
+    alert: DashboardAlertDefinition;
+}
+
+export function AlertItem({ alert, className, ...props }: Readonly<AlertItemProps>) {
+    const { data } = useQuery({
+        queryKey: ['alert', alert.id],
+        queryFn: () => alert.check(),
+        refetchInterval: alert.recheckInterval,
+    });
+
+    const isAlertActive = alert.shouldShow?.(data);
+
+    if (!isAlertActive) {
+        return null;
+    }
+
+    return (
+        <div className={cn('flex items-center justify-between gap-1', className)} {...props}>
+            <div className="flex flex-col">
+                <span className="font-semibold">
+                    {typeof alert.title === 'string' ? alert.title : alert.title(data)}
+                </span>
+                <span className="text-sm text-muted-foreground">
+                    {typeof alert.description === 'string' ? alert.description : alert.description?.(data)}
+                </span>
+            </div>
+            <div className="flex items-center gap-1">
+                {alert.actions?.map(action => (
+                    <Button
+                        key={action.label}
+                        variant="secondary"
+                        size="sm"
+                        onClick={() => action.onClick(data)}
+                    >
+                        {action.label}
+                    </Button>
+                ))}
+            </div>
+        </div>
+    );
+}

+ 23 - 0
packages/dashboard/src/lib/framework/alert/alerts-indicator.tsx

@@ -0,0 +1,23 @@
+import { useQueries } from '@tanstack/react-query';
+import { useAlerts } from './alert-extensions.js';
+
+export function AlertsIndicator() {
+    const { alerts } = useAlerts();
+
+    const alertsCount = useQueries({
+        queries: alerts.map(alert => ({
+            queryKey: ['alert', alert.id],
+            queryFn: () => alert.check(),
+        })),
+        combine: results => {
+            return results.filter((result, idx) => result.data && alerts[idx].shouldShow?.(result.data))
+                .length;
+        },
+    });
+
+    return (
+        <div className="absolute -right-1 -top-1 rounded-full bg-red-500 text-xs w-4 h-4 flex items-center justify-center">
+            {alertsCount}
+        </div>
+    );
+}

+ 13 - 0
packages/dashboard/src/lib/framework/alert/types.ts

@@ -0,0 +1,13 @@
+export interface DashboardAlertDefinition<TResponse = any> {
+    id: string;
+    title: string | ((data: TResponse) => string);
+    description?: string | ((data: TResponse) => string);
+    severity: 'info' | 'warning' | 'error';
+    check: () => Promise<TResponse> | TResponse;
+    recheckInterval?: number;
+    shouldShow?: (data: TResponse) => boolean;
+    actions?: Array<{
+        label: string;
+        onClick: (data: TResponse) => void;
+    }>;
+}

+ 30 - 0
packages/dashboard/src/lib/framework/defaults.ts

@@ -9,6 +9,7 @@ import {
     Users,
 } from 'lucide-react';
 
+import { registerAlert } from './alert/alert-extensions.js';
 import { LatestOrdersWidget } from './dashboard-widget/latest-orders-widget/index.js';
 import { MetricsWidget } from './dashboard-widget/metrics-widget/index.js';
 import { OrdersSummaryWidget } from './dashboard-widget/orders-summary/index.js';
@@ -221,4 +222,33 @@ export function registerDefaults() {
         component: OrdersSummaryWidget,
         defaultSize: { w: 6, h: 3, x: 6, y: 0 },
     });
+
+    // registerAlert<boolean>({
+    //     id: 'test-alert',
+    //     title: data => `Test Alert ${String(data)}`,
+    //     description: 'This is a test alert',
+    //     severity: 'info',
+    //     check: () => Promise.resolve(true),
+    //     actions: [
+    //         {
+    //             label: 'Test Action',
+    //             onClick: () => console.log('Test Action'),
+    //         },
+    //     ],
+    // });
+
+    // registerAlert<boolean>({
+    //     id: 'test-alert-2',
+    //     title: 'Test Alert 2',
+    //     description: 'This is a test alert 2',
+    //     severity: 'info',
+    //     check: () => Promise.resolve(true),
+    //     shouldShow: data => data === true,
+    //     actions: [
+    //         {
+    //             label: 'Test Action',
+    //             onClick: () => console.log('Test Action'),
+    //         },
+    //     ],
+    // });
 }

+ 2 - 0
packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts

@@ -2,6 +2,7 @@ import { NavMenuItem } from '@/framework/nav-menu/nav-menu-extensions.js';
 import { AnyRoute, RouteOptions } from '@tanstack/react-router';
 import React from 'react';
 
+import { DashboardAlertDefinition } from '../alert/types.js';
 import { DashboardWidgetDefinition } from '../dashboard-widget/types.js';
 import { PageContext } from '../layout-engine/page-layout.js';
 
@@ -55,4 +56,5 @@ export interface DashboardExtension {
     widgets: DashboardWidgetDefinition[];
     actionBarItems: DashboardActionBarItem[];
     pageBlocks: DashboardPageBlockDefinition[];
+    alerts: DashboardAlertDefinition[];
 }

+ 2 - 0
packages/dashboard/src/lib/framework/registry/registry-types.ts

@@ -1,3 +1,4 @@
+import { DashboardAlertDefinition } from '../alert/types.js';
 import { DashboardWidgetDefinition } from '../dashboard-widget/types.js';
 import { DashboardActionBarItem } from '../extension-api/extension-api-types.js';
 import { DashboardPageBlockDefinition } from '../extension-api/extension-api-types.js';
@@ -10,6 +11,7 @@ export interface GlobalRegistryContents {
     dashboardActionBarItemRegistry: Map<string, DashboardActionBarItem[]>;
     dashboardPageBlockRegistry: Map<string, DashboardPageBlockDefinition[]>;
     dashboardWidgetRegistry: Map<string, DashboardWidgetDefinition>;
+    dashboardAlertRegistry: Map<string, DashboardAlertDefinition>;
 }
 
 export type GlobalRegistryKey = keyof GlobalRegistryContents;

File diff suppressed because it is too large
+ 1 - 1
packages/dashboard/src/lib/graphql/graphql-env.d.ts


+ 2 - 3
packages/dev-server/dev-config.ts

@@ -20,7 +20,6 @@ import path from 'path';
 import { DataSourceOptions } from 'typeorm';
 
 import { MultivendorPlugin } from './example-plugins/multivendor-plugin/multivendor.plugin';
-import { ReviewsPlugin } from '@plugins/reviews/reviews-plugin';
 
 /**
  * Config settings used during development
@@ -98,7 +97,7 @@ export const devConfig: VendureConfig = {
         tasks: [
             new ScheduledTask({
                 id: 'test-job',
-                description: 'A test job that doesn\'t do anything',
+                description: "A test job that doesn't do anything",
                 schedule: '*/20 * * * * *',
                 async execute(injector) {
                     await new Promise(resolve => setTimeout(resolve, 10_000));
@@ -118,7 +117,7 @@ export const devConfig: VendureConfig = {
         //     platformFeePercent: 10,
         //     platformFeeSKU: 'FEE',
         // }),
-        ReviewsPlugin,
+        // ReviewsPlugin,
         AssetServerPlugin.init({
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'assets'),

File diff suppressed because it is too large
+ 1 - 1
packages/dev-server/graphql/graphql-env.d.ts


Some files were not shown because too many files changed in this diff