Explorar o código

feat(dashboard-plugin): Introduce Dashboard Plugin with metrics API and static file serving

David Höck hai 5 meses
pai
achega
612d527e14

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 318 - 375
package-lock.json


+ 1 - 1
package.json

@@ -45,7 +45,7 @@
     "@swc/core": "^1.4.6",
     "@types/klaw-sync": "^6.0.5",
     "@types/node": "^20.11.19",
-    "concurrently": "^8.2.2",
+    "concurrently": "^9.2.0",
     "conventional-changelog-core": "^7.0.0",
     "cross-env": "^7.0.3",
     "find": "^0.3.0",

+ 1 - 1
packages/admin-ui-plugin/package.json

@@ -33,7 +33,7 @@
         "typescript": "5.8.2"
     },
     "dependencies": {
-        "date-fns": "^2.30.0",
+        "date-fns": "^4.0.0",
         "express-rate-limit": "^7.5.0",
         "fs-extra": "^11.2.0"
     }

+ 71 - 0
packages/dashboard-plugin/README.md

@@ -0,0 +1,71 @@
+# Dashboard Plugin
+
+This plugin serves the static files of the Vendure Dashboard UI and provides the GraphQL extensions needed for the order metrics on the dashboard index page.
+
+## Installation
+
+```bash
+yarn add @vendure/dashboard-plugin
+```
+
+or
+
+```bash
+npm install @vendure/dashboard-plugin
+```
+
+## Usage
+
+### Basic usage - serving the Dashboard UI
+
+```typescript
+import { DashboardPlugin } from '@vendure/dashboard-plugin';
+
+const config: VendureConfig = {
+  // Add an instance of the plugin to the plugins array
+  plugins: [
+    DashboardPlugin.init({ route: 'dashboard' }),
+  ],
+};
+```
+
+The Dashboard UI will be served at the `/dashboard/` path.
+
+### Using only the metrics API
+
+If you are building a stand-alone version of the Dashboard UI app and don't need this plugin to serve the Dashboard UI, you can still use the `metricSummary` query by adding the `DashboardPlugin` to the `plugins` array without calling the `init()` method:
+
+```typescript
+import { DashboardPlugin } from '@vendure/dashboard-plugin';
+
+const config: VendureConfig = {
+  plugins: [
+    DashboardPlugin, // <-- no call to .init()
+  ],
+  // ...
+};
+```
+
+### Custom Dashboard UI build
+
+You can also provide a custom build of the Dashboard UI:
+
+```typescript
+import { DashboardPlugin } from '@vendure/dashboard-plugin';
+
+const config: VendureConfig = {
+  plugins: [
+    DashboardPlugin.init({ 
+      route: 'dashboard',
+      app: path.join(__dirname, 'custom-dashboard-build'),
+    }),
+  ],
+};
+```
+
+## API
+
+### DashboardPluginOptions
+
+- `route: string` - The route at which the Dashboard UI will be served (default: `'dashboard'`)
+- `app?: string` - Optional path to a custom build of the Dashboard UI

+ 40 - 0
packages/dashboard-plugin/package.json

@@ -0,0 +1,40 @@
+{
+    "name": "@vendure/dashboard-plugin",
+    "version": "3.3.7",
+    "main": "dist/index.js",
+    "types": "dist/index.d.ts",
+    "files": [
+        "dist/**/*"
+    ],
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/vendure-ecommerce/vendure"
+    },
+    "license": "GPL-3.0-or-later",
+    "scripts": {
+        "build": "rimraf dist && tsc -p ./tsconfig.build.json",
+        "watch": "tsc -p ./tsconfig.build.json --watch",
+        "lint": "eslint --fix .",
+        "compile": "tsc -p ./tsconfig.build.json"
+    },
+    "homepage": "https://www.vendure.io",
+    "funding": "https://github.com/sponsors/michaelbromley",
+    "publishConfig": {
+        "access": "public"
+    },
+    "devDependencies": {
+        "@types/express": "^5.0.1",
+        "@types/fs-extra": "^11.0.4",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
+        "@vendure/dashboard": "3.3.7",
+        "express": "^5.1.0",
+        "rimraf": "^5.0.5",
+        "typescript": "5.8.2"
+    },
+    "dependencies": {
+        "date-fns": "^4.0.0",
+        "express-rate-limit": "^7.5.0",
+        "fs-extra": "^11.2.0"
+    }
+}

+ 33 - 0
packages/dashboard-plugin/src/api/api-extensions.ts

@@ -0,0 +1,33 @@
+import gql from 'graphql-tag';
+
+export const adminApiExtensions = gql`
+    type MetricSummary {
+        interval: MetricInterval!
+        type: MetricType!
+        title: String!
+        entries: [MetricSummaryEntry!]!
+    }
+    enum MetricInterval {
+        Daily
+    }
+    enum MetricType {
+        OrderCount
+        OrderTotal
+        AverageOrderValue
+    }
+    type MetricSummaryEntry {
+        label: String!
+        value: Float!
+    }
+    input MetricSummaryInput {
+        interval: MetricInterval!
+        types: [MetricType!]!
+        refresh: Boolean
+    }
+    extend type Query {
+        """
+        Get metrics for the given interval and metric types.
+        """
+        metricSummary(input: MetricSummaryInput): [MetricSummary!]!
+    }
+`;

+ 19 - 0
packages/dashboard-plugin/src/api/metrics.resolver.ts

@@ -0,0 +1,19 @@
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
+
+import { MetricsService } from '../service/metrics.service';
+import { MetricSummary, MetricSummaryInput } from '../types';
+
+@Resolver()
+export class MetricsResolver {
+    constructor(private service: MetricsService) {}
+
+    @Query()
+    @Allow(Permission.ReadOrder)
+    async metricSummary(
+        @Ctx() ctx: RequestContext,
+        @Args('input') input: MetricSummaryInput,
+    ): Promise<MetricSummary[]> {
+        return this.service.getMetrics(ctx, input);
+    }
+}

+ 87 - 0
packages/dashboard-plugin/src/config/metrics-strategies.ts

@@ -0,0 +1,87 @@
+import { RequestContext } from '@vendure/core';
+
+import { MetricData } from '../service/metrics.service';
+import { MetricInterval, MetricSummaryEntry, MetricType } from '../types';
+
+/**
+ * Calculate your metric data based on the given input.
+ * Be careful with heavy queries and calculations,
+ * as this function is executed everytime a user views its dashboard
+ *
+ */
+export interface MetricCalculation {
+    type: MetricType;
+
+    getTitle(ctx: RequestContext): string;
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry;
+}
+
+export function getMonthName(monthNr: number): string {
+    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+    return monthNames[monthNr];
+}
+
+/**
+ * Calculates the average order value per month/week
+ */
+export class AverageOrderValueMetric implements MetricCalculation {
+    readonly type = MetricType.AverageOrderValue;
+
+    getTitle(ctx: RequestContext): string {
+        return 'average-order-value';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        if (!data.orders.length) {
+            return {
+                label,
+                value: 0,
+            };
+        }
+        const total = data.orders.map(o => o.totalWithTax).reduce((_total, current) => _total + current);
+        const average = Math.round(total / data.orders.length);
+        return {
+            label,
+            value: average,
+        };
+    }
+}
+
+/**
+ * Calculates number of orders
+ */
+export class OrderCountMetric implements MetricCalculation {
+    readonly type = MetricType.OrderCount;
+
+    getTitle(ctx: RequestContext): string {
+        return 'order-count';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        return {
+            label,
+            value: data.orders.length,
+        };
+    }
+}
+/**
+ * Calculates order total
+ */
+export class OrderTotalMetric implements MetricCalculation {
+    readonly type = MetricType.OrderTotal;
+
+    getTitle(ctx: RequestContext): string {
+        return 'order-totals';
+    }
+
+    calculateEntry(ctx: RequestContext, interval: MetricInterval, data: MetricData): MetricSummaryEntry {
+        const label = data.date.toISOString();
+        return {
+            label,
+            value: data.orders.map(o => o.totalWithTax).reduce((_total, current) => _total + current, 0),
+        };
+    }
+}

+ 8 - 0
packages/dashboard-plugin/src/constants.ts

@@ -0,0 +1,8 @@
+import { join } from 'path';
+
+export const DEFAULT_APP_PATH = join(__dirname, 'dist');
+export const loggerCtx = 'DashboardPlugin';
+export const defaultLanguage = 'en';
+export const defaultLocale = undefined;
+export const defaultAvailableLanguages = ['en', 'de', 'es', 'cs', 'zh_Hans', 'pt_BR', 'pt_PT', 'zh_Hant'];
+export const defaultAvailableLocales = ['en-US', 'de-DE', 'es-ES', 'zh-CN', 'zh-TW', 'pt-BR', 'pt-PT'];

+ 2 - 0
packages/dashboard-plugin/src/index.ts

@@ -0,0 +1,2 @@
+export * from './plugin';
+export * from './types';

+ 181 - 0
packages/dashboard-plugin/src/plugin.ts

@@ -0,0 +1,181 @@
+import { MiddlewareConsumer, NestModule } from '@nestjs/common';
+import { Type } from '@vendure/common/lib/shared-types';
+import {
+    ConfigService,
+    Logger,
+    PluginCommonModule,
+    ProcessContext,
+    registerPluginStartupMessage,
+    VendurePlugin,
+} from '@vendure/core';
+import express from 'express';
+import { rateLimit } from 'express-rate-limit';
+import fs from 'fs-extra';
+import path from 'path';
+
+import { adminApiExtensions } from './api/api-extensions';
+import { MetricsResolver } from './api/metrics.resolver';
+import { DEFAULT_APP_PATH, loggerCtx } from './constants';
+import { MetricsService } from './service/metrics.service';
+
+/**
+ * @description
+ * Configuration options for the {@link DashboardPlugin}.
+ *
+ * @docsCategory core plugins/DashboardPlugin
+ */
+export interface DashboardPluginOptions {
+    /**
+     * @description
+     * The route to the Dashboard UI.
+     *
+     * @default 'dashboard'
+     */
+    route: string;
+    /**
+     * @description
+     * The path to the dashboard UI app dist directory. By default, the built-in dashboard UI
+     * will be served. This can be overridden with a custom build of the dashboard.
+     */
+    app?: string;
+}
+
+/**
+ * @description
+ * This plugin serves the static files of the Vendure Dashboard and provides the
+ * GraphQL extensions needed for the order metrics on the dashboard index page.
+ *
+ * ## Installation
+ *
+ * `yarn add \@vendure/dashboard-plugin`
+ *
+ * or
+ *
+ * `npm install \@vendure/dashboard-plugin`
+ *
+ * @example
+ * ```ts
+ * import { DashboardPlugin } from '\@vendure/dashboard-plugin';
+ *
+ * const config: VendureConfig = {
+ *   // Add an instance of the plugin to the plugins array
+ *   plugins: [
+ *     DashboardPlugin.init({ route: 'dashboard' }),
+ *   ],
+ * };
+ * ```
+ *
+ * ## Metrics
+ *
+ * This plugin defines a `metricSummary` query which is used by the Dashboard UI to
+ * display the order metrics on the dashboard.
+ *
+ * If you are building a stand-alone version of the Dashboard UI app, and therefore
+ * don't need this plugin to serve the Dashboard UI, you can still use the
+ * `metricSummary` query by adding the `DashboardPlugin` to the `plugins` array,
+ * but without calling the `init()` method:
+ *
+ * @example
+ * ```ts
+ * import { DashboardPlugin } from '\@vendure/dashboard-plugin';
+ *
+ * const config: VendureConfig = {
+ *   plugins: [
+ *     DashboardPlugin, // <-- no call to .init()
+ *   ],
+ *   // ...
+ * };
+ * ```
+ *
+ * @docsCategory core plugins/DashboardPlugin
+ */
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [MetricsResolver],
+    },
+    providers: [MetricsService],
+    compatibility: '^3.0.0',
+})
+export class DashboardPlugin implements NestModule {
+    private static options: DashboardPluginOptions | undefined;
+
+    constructor(
+        private configService: ConfigService,
+        private processContext: ProcessContext,
+    ) {}
+
+    /**
+     * @description
+     * Set the plugin options
+     */
+    static init(options: DashboardPluginOptions): Type<DashboardPlugin> {
+        this.options = options;
+        return DashboardPlugin;
+    }
+
+    configure(consumer: MiddlewareConsumer) {
+        if (this.processContext.isWorker) {
+            return;
+        }
+        if (!DashboardPlugin.options) {
+            Logger.info(
+                `DashboardPlugin's init() method was not called. The Dashboard UI will not be served.`,
+                loggerCtx,
+            );
+            return;
+        }
+        const { route, app } = DashboardPlugin.options;
+        const dashboardPath = app || this.getDashboardPath();
+
+        Logger.info('Creating dashboard middleware', loggerCtx);
+        consumer.apply(this.createStaticServer(dashboardPath)).forRoutes(route);
+
+        registerPluginStartupMessage('Dashboard UI', route);
+    }
+
+    private createStaticServer(dashboardPath: string) {
+        const limiter = rateLimit({
+            windowMs: 60 * 1000,
+            limit: process.env.NODE_ENV === 'production' ? 500 : 2000,
+            standardHeaders: true,
+            legacyHeaders: false,
+        });
+
+        const dashboardServer = express.Router();
+        // This is a workaround for a type mismatch between express v5 (Vendure core)
+        // and express v4 (several transitive dependencies). Can be removed once the
+        // ecosystem has more significantly shifted to v5.
+        dashboardServer.use(limiter as any);
+        dashboardServer.use(express.static(dashboardPath));
+        dashboardServer.use((req, res) => {
+            res.sendFile('index.html', { root: dashboardPath });
+        });
+
+        return dashboardServer;
+    }
+
+    private getDashboardPath(): string {
+        // First, try to find the dashboard dist directory in the @vendure/dashboard package
+        try {
+            const dashboardPackageJson = require.resolve('@vendure/dashboard/package.json');
+            const dashboardPackageRoot = path.dirname(dashboardPackageJson);
+            const dashboardDistPath = path.join(dashboardPackageRoot, 'dist');
+
+            if (fs.existsSync(dashboardDistPath)) {
+                Logger.info(`Found dashboard UI at ${dashboardDistPath}`, loggerCtx);
+                return dashboardDistPath;
+            }
+        } catch (e) {
+            // Dashboard package not found, continue to fallback
+        }
+
+        // Fallback to default path
+        Logger.warn(
+            `Could not find @vendure/dashboard dist directory. Falling back to default path: ${DEFAULT_APP_PATH}`,
+            loggerCtx,
+        );
+        return DEFAULT_APP_PATH;
+    }
+}

+ 175 - 0
packages/dashboard-plugin/src/service/metrics.service.ts

@@ -0,0 +1,175 @@
+import { Injectable } from '@nestjs/common';
+import { assertNever } from '@vendure/common/lib/shared-utils';
+import { CacheService, Logger, Order, RequestContext, TransactionalConnection } from '@vendure/core';
+import { createHash } from 'crypto';
+import {
+    Duration,
+    endOfDay,
+    getDayOfYear,
+    getISOWeek,
+    getMonth,
+    setDayOfYear,
+    startOfDay,
+    sub,
+} from 'date-fns';
+
+import {
+    AverageOrderValueMetric,
+    MetricCalculation,
+    OrderCountMetric,
+    OrderTotalMetric,
+} from '../config/metrics-strategies';
+import { loggerCtx } from '../constants';
+import { MetricInterval, MetricSummary, MetricSummaryEntry, MetricSummaryInput } from '../types';
+
+export type MetricData = {
+    date: Date;
+    orders: Order[];
+};
+
+@Injectable()
+export class MetricsService {
+    metricCalculations: MetricCalculation[];
+
+    constructor(
+        private connection: TransactionalConnection,
+        private cacheService: CacheService,
+    ) {
+        this.metricCalculations = [
+            new AverageOrderValueMetric(),
+            new OrderCountMetric(),
+            new OrderTotalMetric(),
+        ];
+    }
+
+    async getMetrics(
+        ctx: RequestContext,
+        { interval, types, refresh }: MetricSummaryInput,
+    ): Promise<MetricSummary[]> {
+        // Set 23:59:59.999 as endDate
+        const endDate = endOfDay(new Date());
+        // Check if we have cached result
+        const hash = createHash('sha1')
+            .update(
+                JSON.stringify({
+                    endDate,
+                    types: types.sort(),
+                    interval,
+                    channel: ctx.channel.token,
+                }),
+            )
+            .digest('base64');
+        const cacheKey = `MetricsService:${hash}`;
+        const cachedMetricList = await this.cacheService.get<MetricSummary[]>(cacheKey);
+        if (cachedMetricList && refresh !== true) {
+            Logger.verbose(`Returning cached metrics for channel ${ctx.channel.token}`, loggerCtx);
+            return cachedMetricList;
+        }
+        // No cache, calculating new metrics
+        Logger.verbose(
+            `No cache hit, calculating ${interval} metrics until ${endDate.toISOString()} for channel ${
+                ctx.channel.token
+            } for all orders`,
+            loggerCtx,
+        );
+        const data = await this.loadData(ctx, interval, endDate);
+        const metrics: MetricSummary[] = [];
+        for (const type of types) {
+            const metric = this.metricCalculations.find(m => m.type === type);
+            if (!metric) {
+                continue;
+            }
+            // Calculate entry (month or week)
+            const entries: MetricSummaryEntry[] = [];
+            data.forEach(dataPerTick => {
+                entries.push(metric.calculateEntry(ctx, interval, dataPerTick));
+            });
+            // Create metric with calculated entries
+            metrics.push({
+                interval,
+                title: metric.getTitle(ctx),
+                type: metric.type,
+                entries,
+            });
+        }
+        await this.cacheService.set(cacheKey, metrics, { ttl: 1000 * 60 * 60 * 24 });
+        return metrics;
+    }
+
+    async loadData(
+        ctx: RequestContext,
+        interval: MetricInterval,
+        endDate: Date,
+    ): Promise<Map<number, MetricData>> {
+        let nrOfEntries: number;
+        let backInTimeAmount: Duration;
+        const orderRepo = this.connection.getRepository(ctx, Order);
+        // What function to use to get the current Tick of a date (i.e. the week or month number)
+        let getTickNrFn: typeof getMonth | typeof getISOWeek;
+        let maxTick: number;
+        switch (interval) {
+            case MetricInterval.Daily: {
+                nrOfEntries = 30;
+                backInTimeAmount = { days: nrOfEntries };
+                getTickNrFn = getDayOfYear;
+                maxTick = 365;
+                break;
+            }
+            default:
+                assertNever(interval);
+        }
+        const startDate = startOfDay(sub(endDate, backInTimeAmount));
+        const startTick = getTickNrFn(startDate);
+        // Get orders in a loop until we have all
+        let skip = 0;
+        const take = 1000;
+        let hasMoreOrders = true;
+        const orders: Order[] = [];
+        while (hasMoreOrders) {
+            const query = orderRepo
+                .createQueryBuilder('order')
+                .leftJoin('order.channels', 'orderChannel')
+                .where('orderChannel.id=:channelId', { channelId: ctx.channelId })
+                .andWhere('order.orderPlacedAt >= :startDate', {
+                    startDate: startDate.toISOString(),
+                })
+                .skip(skip)
+                .take(take);
+            const [items, nrOfOrders] = await query.getManyAndCount();
+            orders.push(...items);
+            Logger.verbose(
+                `Fetched orders ${skip}-${skip + take} for channel ${
+                    ctx.channel.token
+                } for ${interval} metrics`,
+                loggerCtx,
+            );
+            skip += items.length;
+            if (orders.length >= nrOfOrders) {
+                hasMoreOrders = false;
+            }
+        }
+        Logger.verbose(
+            `Finished fetching all ${orders.length} orders for channel ${ctx.channel.token} for ${interval} metrics`,
+            loggerCtx,
+        );
+        const dataPerInterval = new Map<number, MetricData>();
+        const ticks = [];
+        for (let i = 1; i <= nrOfEntries; i++) {
+            if (startTick + i >= maxTick) {
+                // make sure we don't go over month 12 or week 52
+                ticks.push(startTick + i - maxTick);
+            } else {
+                ticks.push(startTick + i);
+            }
+        }
+        ticks.forEach(tick => {
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            const ordersInCurrentTick = orders.filter(order => getTickNrFn(order.orderPlacedAt!) === tick);
+            dataPerInterval.set(tick, {
+                orders: ordersInCurrentTick,
+                date: setDayOfYear(endDate, tick),
+            });
+        });
+        return dataPerInterval;
+    }
+}

+ 27 - 0
packages/dashboard-plugin/src/types.ts

@@ -0,0 +1,27 @@
+export type MetricSummary = {
+    interval: MetricInterval;
+    type: MetricType;
+    title: string;
+    entries: MetricSummaryEntry[];
+};
+
+export enum MetricType {
+    OrderCount = 'OrderCount',
+    OrderTotal = 'OrderTotal',
+    AverageOrderValue = 'AverageOrderValue',
+}
+
+export enum MetricInterval {
+    Daily = 'Daily',
+}
+
+export type MetricSummaryEntry = {
+    label: string;
+    value: number;
+};
+
+export interface MetricSummaryInput {
+    interval: MetricInterval;
+    types: MetricType[];
+    refresh?: boolean;
+}

+ 10 - 0
packages/dashboard-plugin/tsconfig.build.json

@@ -0,0 +1,10 @@
+{
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "composite": false,
+        "incremental": false,
+        "paths": {}
+    },
+    "include": ["src/**/*"],
+    "exclude": ["**/*.spec.ts", "e2e/**"]
+}

+ 12 - 0
packages/dashboard-plugin/tsconfig.json

@@ -0,0 +1,12 @@
+{
+    "extends": "../../tsconfig.json",
+    "compilerOptions": {
+        "baseUrl": ".",
+        "rootDir": "./src",
+        "outDir": "./dist",
+        "declaration": true,
+        "removeComments": true,
+        "strictPropertyInitialization": false,
+        "sourceMap": true
+    }
+}

+ 1 - 1
packages/dashboard/index.html

@@ -2,7 +2,7 @@
 <html lang="en">
     <head>
         <meta charset="UTF-8" />
-        <link rel="icon" type="image/svg+xml" href="/favicon.png" />
+        <link rel="icon" type="image/png" href="/favicon.png" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         <meta name="description" content="Vendure Admin Dashboard" />
         <meta name="author" content="Vendure" />

+ 1 - 1
packages/dashboard/package.json

@@ -104,7 +104,7 @@
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
         "cmdk": "^1.1.1",
-        "date-fns": "^3.6.0",
+        "date-fns": "^4.0.0",
         "embla-carousel-react": "^8.6.0",
         "fast-glob": "^3.3.2",
         "gql.tada": "^1.8.10",

+ 28 - 24
packages/dev-server/dev-config.ts

@@ -1,5 +1,4 @@
 /* eslint-disable no-console */
-import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import { ADMIN_API_PATH, API_PORT, SHOP_API_PATH } from '@vendure/common/lib/shared-constants';
 import {
@@ -13,6 +12,7 @@ import {
     SettingsStoreScopes,
     VendureConfig,
 } from '@vendure/core';
+import { DashboardPlugin } from '@vendure/dashboard-plugin';
 import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
 import { GraphiqlPlugin } from '@vendure/graphiql-plugin';
 import { TelemetryPlugin } from '@vendure/telemetry-plugin';
@@ -140,29 +140,33 @@ export const devConfig: VendureConfig = {
             },
         }),
         ...(IS_INSTRUMENTED ? [TelemetryPlugin.init({})] : []),
-        AdminUiPlugin.init({
-            route: 'admin',
-            port: 5001,
-            adminUiConfig: {},
-            // Un-comment to compile a custom admin ui
-            // app: compileUiExtensions({
-            //     outputPath: path.join(__dirname, './custom-admin-ui'),
-            //     extensions: [
-            //         {
-            //             id: 'ui-extensions-library',
-            //             extensionPath: path.join(__dirname, 'example-plugins/ui-extensions-library/ui'),
-            //             routes: [{ route: 'ui-library', filePath: 'routes.ts' }],
-            //             providers: ['providers.ts'],
-            //         },
-            //         {
-            //             globalStyles: path.join(
-            //                 __dirname,
-            //                 'test-plugins/with-ui-extension/ui/custom-theme.scss',
-            //             ),
-            //         },
-            //     ],
-            //     devMode: true,
-            // }),
+        // AdminUiPlugin.init({
+        //     route: 'admin',
+        //     port: 5001,
+        //     adminUiConfig: {},
+        //     // Un-comment to compile a custom admin ui
+        //     // app: compileUiExtensions({
+        //     //     outputPath: path.join(__dirname, './custom-admin-ui'),
+        //     //     extensions: [
+        //     //         {
+        //     //             id: 'ui-extensions-library',
+        //     //             extensionPath: path.join(__dirname, 'example-plugins/ui-extensions-library/ui'),
+        //     //             routes: [{ route: 'ui-library', filePath: 'routes.ts' }],
+        //     //             providers: ['providers.ts'],
+        //     //         },
+        //     //         {
+        //     //             globalStyles: path.join(
+        //     //                 __dirname,
+        //     //                 'test-plugins/with-ui-extension/ui/custom-theme.scss',
+        //     //             ),
+        //     //         },
+        //     //     ],
+        //     //     devMode: true,
+        //     // }),
+        // }),
+        DashboardPlugin.init({
+            route: 'dashboard',
+            app: path.join(__dirname, './dist'),
         }),
     ],
 };

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2 - 2
packages/dev-server/graphql/graphql-env.d.ts


+ 1 - 1
packages/dev-server/package.json

@@ -31,7 +31,7 @@
         "@vendure/testing": "3.3.7",
         "@vendure/ui-devkit": "3.3.7",
         "commander": "^12.0.0",
-        "concurrently": "^8.2.2",
+        "concurrently": "^9.2.0",
         "csv-stringify": "^6.4.6",
         "dayjs": "^1.11.10",
         "jsdom": "^26.0.0",

+ 1 - 1
packages/dev-server/vite.config.mts

@@ -10,6 +10,6 @@ export default defineConfig({
             vendureConfigPath: pathToFileURL('./dev-config.ts'),
             adminUiConfig: { apiHost: 'http://localhost', apiPort: 3000 },
             gqlOutputPath: path.resolve(__dirname, './graphql/'),
-        }) as any,
+        }),
     ],
 });

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio