Parcourir la source

feat(dashboard): Add DashboardPlugin as part of dashboard package (#3711)

Co-authored-by: David Höck <d.hoeck@elevantiq.com>
Michael Bromley il y a 5 mois
Parent
commit
49d449f18a

+ 1 - 1
.github/workflows/scripts/vite.config.mts

@@ -1,4 +1,4 @@
-import { vendureDashboardPlugin } from '@vendure/dashboard/plugin';
+import { vendureDashboardPlugin } from '@vendure/dashboard/vite';
 import { join, resolve } from 'path';
 import { pathToFileURL } from 'url';
 import { defineConfig } from 'vite';

+ 1 - 1
docs/docs/guides/extending-the-dashboard/getting-started/index.md

@@ -37,7 +37,7 @@ npm install @vendure/dashboard
 Then create a `vite.config.mts` file in the root of your project with the following content:
 
 ```ts title="vite.config.mts"
-import { vendureDashboardPlugin } from '@vendure/dashboard/plugin';
+import { vendureDashboardPlugin } from '@vendure/dashboard/vite';
 import { pathToFileURL } from 'url';
 import { defineConfig } from 'vite';
 import { resolve, join } from 'path';

+ 56 - 60
package-lock.json

@@ -29,7 +29,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",
@@ -10937,27 +10937,18 @@
       }
     },
     "node_modules/@mollie/api-client": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/@mollie/api-client/-/api-client-3.7.0.tgz",
-      "integrity": "sha512-8pYq08xwv7VJIQvOJU+3nThZbXuNLhtD8iJQQ4UYW67kxHw4eC6cVE9SNVtUuHRfxTwcKLv5wrZd9jFb3BFqiA==",
+      "version": "4.3.3",
+      "resolved": "https://registry.npmjs.org/@mollie/api-client/-/api-client-4.3.3.tgz",
+      "integrity": "sha512-vA8EM3nO8pcTX2AAG93FQElZ9uVpKJ7DBCEalvyStZEnGYGD3fIVPs6Pmr0lIB2tkjrqjEdbhfwwaK4MtBxs+w==",
       "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
-        "axios": "^0.27.2"
+        "@types/node-fetch": "^2.6.12",
+        "node-fetch": "^2.7.0",
+        "ruply": "^1.0.1"
       },
       "engines": {
-        "node": ">=6.14"
-      }
-    },
-    "node_modules/@mollie/api-client/node_modules/axios": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
-      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "follow-redirects": "^1.14.9",
-        "form-data": "^4.0.0"
+        "node": ">=8"
       }
     },
     "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
@@ -24252,18 +24243,16 @@
       }
     },
     "node_modules/concurrently": {
-      "version": "8.2.2",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
-      "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
+      "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "chalk": "^4.1.2",
-        "date-fns": "^2.30.0",
         "lodash": "^4.17.21",
         "rxjs": "^7.8.1",
         "shell-quote": "^1.8.1",
-        "spawn-command": "0.0.2",
         "supports-color": "^8.1.1",
         "tree-kill": "^1.2.2",
         "yargs": "^17.7.2"
@@ -24273,7 +24262,7 @@
         "concurrently": "dist/bin/concurrently.js"
       },
       "engines": {
-        "node": "^14.13.0 || >=16.0.0"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
@@ -24325,23 +24314,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/concurrently/node_modules/date-fns": {
-      "version": "2.30.0",
-      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
-      "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/runtime": "^7.21.0"
-      },
-      "engines": {
-        "node": ">=0.11"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/date-fns"
-      }
-    },
     "node_modules/confbox": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@@ -43496,6 +43468,13 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/ruply": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ruply/-/ruply-1.0.1.tgz",
+      "integrity": "sha512-p39LnaaJyuucPGlgaB0KiyifpcuOkn24+Hq5y0ejAD/LlH+mRAbkHn2tckCLgHir+S+nis1WYG+TYEC4zHX0WQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/rxjs": {
       "version": "7.8.2",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -44775,12 +44754,6 @@
         "webidl-conversions": "^4.0.2"
       }
     },
-    "node_modules/spawn-command": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
-      "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
-      "dev": true
-    },
     "node_modules/spdx-correct": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@@ -50515,7 +50488,7 @@
       "version": "3.3.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
-        "date-fns": "^2.30.0",
+        "date-fns": "^4.0.0",
         "express-rate-limit": "^7.5.0",
         "fs-extra": "^11.2.0"
       },
@@ -50534,17 +50507,13 @@
       }
     },
     "packages/admin-ui-plugin/node_modules/date-fns": {
-      "version": "2.30.0",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
       "license": "MIT",
-      "dependencies": {
-        "@babel/runtime": "^7.21.0"
-      },
-      "engines": {
-        "node": ">=0.11"
-      },
       "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/date-fns"
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
       }
     },
     "packages/admin-ui/node_modules/@angular-eslint/eslint-plugin-template": {
@@ -50698,6 +50667,7 @@
         "dotenv": "^16.4.5",
         "fs-extra": "^11.2.0",
         "picocolors": "^1.0.0",
+        "strip-json-comments": "^5.0.2",
         "ts-morph": "^21.0.1",
         "ts-node": "^10.9.2",
         "tsconfig-paths": "^4.2.0"
@@ -50707,6 +50677,8 @@
       },
       "devDependencies": {
         "@vendure/core": "3.3.7",
+        "@vendure/testing": "3.3.7",
+        "cross-env": "^7.0.3",
         "typescript": "5.8.2"
       },
       "funding": {
@@ -50722,6 +50694,18 @@
         "node": ">=16"
       }
     },
+    "packages/cli/node_modules/strip-json-comments": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.2.tgz",
+      "integrity": "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "packages/common": {
       "name": "@vendure/common",
       "version": "3.3.7",
@@ -51067,9 +51051,11 @@
         "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",
+        "express-rate-limit": "^7.5.0",
         "fast-glob": "^3.3.2",
+        "fs-extra": "^11.2.0",
         "gql.tada": "^1.8.10",
         "graphql": "^16.10.0",
         "input-otp": "^1.4.2",
@@ -51212,6 +51198,16 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "packages/dashboard/node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
+      }
+    },
     "packages/dashboard/node_modules/eslint": {
       "version": "9.25.1",
       "dev": true,
@@ -51453,7 +51449,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",
@@ -51615,7 +51611,7 @@
         "currency.js": "2.0.4"
       },
       "devDependencies": {
-        "@mollie/api-client": "^3.7.0",
+        "@mollie/api-client": "^4.3.3",
         "@types/braintree": "^3.3.11",
         "@types/localtunnel": "2.0.4",
         "@vendure/common": "3.3.7",
@@ -51632,7 +51628,7 @@
         "url": "https://github.com/sponsors/michaelbromley"
       },
       "peerDependencies": {
-        "@mollie/api-client": "3.x",
+        "@mollie/api-client": "4.x",
         "braintree": "3.x",
         "stripe": "13.x"
       },

+ 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"
     }

+ 5 - 1
packages/admin-ui-plugin/src/api/api-extensions.ts

@@ -1,6 +1,6 @@
 import gql from 'graphql-tag';
 
-export const adminApiExtensions = gql`
+export const metricsApiExtensions = gql`
     type MetricSummary {
         interval: MetricInterval!
         type: MetricType!
@@ -31,3 +31,7 @@ export const adminApiExtensions = gql`
         metricSummary(input: MetricSummaryInput): [MetricSummary!]!
     }
 `;
+
+export function getApiExtensions(compatibilityMode: boolean) {
+    return compatibilityMode ? undefined : metricsApiExtensions;
+}

+ 18 - 3
packages/admin-ui-plugin/src/plugin.ts

@@ -23,7 +23,7 @@ import { rateLimit } from 'express-rate-limit';
 import fs from 'fs-extra';
 import path from 'path';
 
-import { adminApiExtensions } from './api/api-extensions';
+import { getApiExtensions } from './api/api-extensions';
 import { MetricsResolver } from './api/metrics.resolver';
 import {
     DEFAULT_APP_PATH,
@@ -77,6 +77,15 @@ export interface AdminUiPluginOptions {
      * for specifying the Vendure GraphQL API host, available UI languages, etc.
      */
     adminUiConfig?: Partial<AdminUiConfig>;
+    /**
+     * @description
+     * If you are running the AdminUiPlugin at the same time as the new {@link DashboardPlugin}, you should
+     * set this to `true` in order to avoid a conflict caused by both plugins defining the same
+     * schema extensions.
+     *
+     * @since 3.4.0
+     */
+    compatibilityMode?: boolean;
 }
 
 /**
@@ -130,8 +139,14 @@ export interface AdminUiPluginOptions {
 @VendurePlugin({
     imports: [PluginCommonModule],
     adminApiExtensions: {
-        schema: adminApiExtensions,
-        resolvers: [MetricsResolver],
+        schema: () => {
+            const compatibilityMode = !!AdminUiPlugin.options?.compatibilityMode;
+            return getApiExtensions(compatibilityMode);
+        },
+        resolvers: () => {
+            const compatibilityMode = !!AdminUiPlugin.options?.compatibilityMode;
+            return compatibilityMode ? [] : [MetricsResolver];
+        },
     },
     providers: [MetricsService],
     compatibility: '^3.0.0',

+ 1 - 1
packages/cli/package.json

@@ -23,7 +23,7 @@
         "watch": "tsc -p ./tsconfig.cli.json --watch",
         "ci": "npm run build",
         "test": "vitest --config vitest.config.mts --run",
-        "e2e": "cross-env PACKAGE=cli vitest --config ../../e2e-common/vitest.config.mts --run",
+        "e2e": "cross-env PACKAGE=cli vitest --config ../../e2e-common/vitest.config.mts --run --pool=forks",
         "e2e:watch": "cross-env PACKAGE=cli vitest --config ../../e2e-common/vitest.config.mts"
     },
     "publishConfig": {

+ 62 - 0
packages/dashboard/README.md

@@ -3,6 +3,68 @@
 This is a React-based admin dashboard for Vendure. It is a standalone application that can be extended to suit the
 needs of any Vendure project.
 
+The package consists of three main components:
+
+- `@vendure/dashboard`: Dashboard source code
+- `@vendure/dashboard/vite`: A Vite plugin that is used to compile the dashboard in your project
+- `@vendure/dashboard/plugin`: A Vendure plugin that provides backend functionality used by the dashboard app.
+
+## DashboardPlugin
+
+### 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
+
 ## Development
 
 Run `npx vite` to start Vite in dev mode.

+ 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" />

+ 153 - 144
packages/dashboard/package.json

@@ -1,149 +1,158 @@
 {
-    "name": "@vendure/dashboard",
-    "private": false,
-    "version": "3.3.7",
-    "type": "module",
-    "repository": {
-        "type": "git",
-        "url": "https://github.com/vendure-ecommerce/vendure"
+  "name": "@vendure/dashboard",
+  "private": false,
+  "version": "3.3.7",
+  "type": "module",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/vendure-ecommerce/vendure"
+  },
+  "homepage": "https://www.vendure.io",
+  "funding": "https://github.com/sponsors/michaelbromley",
+  "publishConfig": {
+    "access": "public"
+  },
+  "scripts": {
+    "dev": "vite",
+    "build:standalone": "vite build",
+    "build:vite": "tsc --project tsconfig.vite.json",
+    "build:plugin": "tsc --project tsconfig.plugin.json && node scripts/build-plugin.js",
+    "build": "npm run build:vite && npm run build:plugin",
+    "watch": "tsc --project tsconfig.vite.json --watch",
+    "test": "vitest run",
+    "lint": "eslint .",
+    "preview": "vite preview",
+    "generate-index": "node scripts/generate-index.js"
+  },
+  "module": "./src/lib/index.ts",
+  "main": "./src/lib/index.ts",
+  "types": "./src/lib/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./src/lib/index.d.ts",
+      "import": "./src/lib/index.ts",
+      "require": "./src/lib/index.ts"
     },
-    "homepage": "https://www.vendure.io",
-    "funding": "https://github.com/sponsors/michaelbromley",
-    "publishConfig": {
-        "access": "public"
+    "./plugin": {
+      "types": "./dist/plugin/index.d.ts",
+      "import": "./dist/plugin/index.js",
+      "require": "./dist/plugin/index.js"
     },
-    "scripts": {
-        "dev": "vite",
-        "build:standalone": "vite build",
-        "build": "tsc --project tsconfig.plugin.json",
-        "watch": "tsc --project tsconfig.plugin.json --watch",
-        "test": "vitest run",
-        "lint": "eslint .",
-        "preview": "vite preview",
-        "generate-index": "node scripts/generate-index.js"
-    },
-    "module": "./src/lib/index.ts",
-    "main": "./src/lib/index.ts",
-    "types": "./src/lib/index.d.ts",
-    "exports": {
-        ".": {
-            "types": "./src/lib/index.d.ts",
-            "import": "./src/lib/index.ts",
-            "require": "./src/lib/index.ts"
-        },
-        "./plugin": {
-            "types": "./dist/plugin/index.d.ts",
-            "import": "./dist/plugin/index.js",
-            "require": "./dist/plugin/index.js"
-        }
-    },
-    "files": [
-        "dist",
-        "src",
-        "vite",
-        "lingui.config.js",
-        "index.html"
-    ],
-    "dependencies": {
-        "@dnd-kit/core": "^6.3.1",
-        "@dnd-kit/modifiers": "^9.0.0",
-        "@dnd-kit/sortable": "^10.0.0",
-        "@hookform/resolvers": "^4.1.3",
-        "@lingui/babel-plugin-lingui-macro": "^5.2.0",
-        "@lingui/cli": "^5.2.0",
-        "@lingui/core": "^5.2.0",
-        "@lingui/react": "^5.2.0",
-        "@lingui/vite-plugin": "^5.2.0",
-        "@radix-ui/react-accordion": "^1.2.11",
-        "@radix-ui/react-alert-dialog": "^1.1.14",
-        "@radix-ui/react-aspect-ratio": "^1.1.7",
-        "@radix-ui/react-avatar": "^1.1.10",
-        "@radix-ui/react-checkbox": "^1.3.2",
-        "@radix-ui/react-collapsible": "^1.1.11",
-        "@radix-ui/react-context-menu": "^2.2.15",
-        "@radix-ui/react-dialog": "^1.1.14",
-        "@radix-ui/react-dropdown-menu": "^2.1.15",
-        "@radix-ui/react-hover-card": "^1.1.14",
-        "@radix-ui/react-label": "^2.1.7",
-        "@radix-ui/react-menubar": "^1.1.15",
-        "@radix-ui/react-navigation-menu": "^1.2.13",
-        "@radix-ui/react-popover": "^1.1.14",
-        "@radix-ui/react-progress": "^1.1.7",
-        "@radix-ui/react-radio-group": "^1.3.7",
-        "@radix-ui/react-scroll-area": "^1.2.9",
-        "@radix-ui/react-select": "^2.2.5",
-        "@radix-ui/react-separator": "^1.1.7",
-        "@radix-ui/react-slider": "^1.3.5",
-        "@radix-ui/react-slot": "^1.2.3",
-        "@radix-ui/react-switch": "^1.2.5",
-        "@radix-ui/react-tabs": "^1.1.12",
-        "@radix-ui/react-toggle": "^1.1.9",
-        "@radix-ui/react-toggle-group": "^1.1.10",
-        "@radix-ui/react-tooltip": "^1.2.7",
-        "@tailwindcss/vite": "^4.1.5",
-        "@tanstack/eslint-plugin-query": "^5.66.1",
-        "@tanstack/react-query": "^5.66.7",
-        "@tanstack/react-query-devtools": "^5.68.0",
-        "@tanstack/react-router": "^1.105.0",
-        "@tanstack/react-table": "^8.21.2",
-        "@tanstack/router-devtools": "^1.105.0",
-        "@tanstack/router-plugin": "^1.105.0",
-        "@tiptap/pm": "^2.11.5",
-        "@tiptap/react": "^2.11.5",
-        "@tiptap/starter-kit": "^2.11.5",
-        "@types/react": "^19.0.10",
-        "@types/react-dom": "^19.0.4",
-        "@types/react-grid-layout": "^1.3.5",
-        "@uidotdev/usehooks": "^2.4.1",
-        "@vendure/common": "3.3.7",
-        "@vendure/core": "3.3.7",
-        "@vitejs/plugin-react": "^4.3.4",
-        "acorn": "^8.11.3",
-        "acorn-walk": "^8.3.2",
-        "awesome-graphql-client": "^2.1.0",
-        "class-variance-authority": "^0.7.1",
-        "clsx": "^2.1.1",
-        "cmdk": "^1.1.1",
-        "date-fns": "^3.6.0",
-        "embla-carousel-react": "^8.6.0",
-        "fast-glob": "^3.3.2",
-        "gql.tada": "^1.8.10",
-        "graphql": "^16.10.0",
-        "input-otp": "^1.4.2",
-        "json-edit-react": "^1.23.1",
-        "lucide-react": "^0.475.0",
-        "motion": "^12.6.2",
-        "next-themes": "^0.4.6",
-        "react": "^19.0.0",
-        "react-day-picker": "^9.8.0",
-        "react-dom": "^19.0.0",
-        "react-dropzone": "^14.3.8",
-        "react-grid-layout": "^1.5.1",
-        "react-hook-form": "^7.60.0",
-        "react-resizable-panels": "^3.0.3",
-        "recharts": "^2.15.4",
-        "sonner": "^2.0.6",
-        "tailwind-merge": "^3.2.0",
-        "tailwindcss": "^4.1.5",
-        "tailwindcss-animate": "^1.0.7",
-        "tsconfig-paths": "^4.2.0",
-        "tw-animate-css": "^1.2.9",
-        "vaul": "^1.1.2",
-        "vite": "^6.3.5",
-        "zod": "^3.25.76"
-    },
-    "devDependencies": {
-        "@eslint/js": "^9.19.0",
-        "@types/node": "^22.13.4",
-        "eslint": "^9.19.0",
-        "eslint-plugin-react": "^7.37.4",
-        "eslint-plugin-react-hooks": "^5.0.0",
-        "eslint-plugin-react-refresh": "^0.4.18",
-        "globals": "^15.14.0",
-        "vite-plugin-dts": "^4.5.3"
-    },
-    "optionalDependencies": {
-        "lightningcss-linux-arm64-musl": "^1.29.3",
-        "lightningcss-linux-x64-musl": "^1.29.1"
+    "./vite": {
+      "types": "./dist/vite/index.d.ts",
+      "import": "./dist/vite/index.js",
+      "require": "./dist/vite/index.js"
     }
+  },
+  "files": [
+    "dist",
+    "src",
+    "vite",
+    "lingui.config.js",
+    "index.html"
+  ],
+  "dependencies": {
+    "@dnd-kit/core": "^6.3.1",
+    "@dnd-kit/modifiers": "^9.0.0",
+    "@dnd-kit/sortable": "^10.0.0",
+    "@hookform/resolvers": "^4.1.3",
+    "@lingui/babel-plugin-lingui-macro": "^5.2.0",
+    "@lingui/cli": "^5.2.0",
+    "@lingui/core": "^5.2.0",
+    "@lingui/react": "^5.2.0",
+    "@lingui/vite-plugin": "^5.2.0",
+    "@radix-ui/react-accordion": "^1.2.11",
+    "@radix-ui/react-alert-dialog": "^1.1.14",
+    "@radix-ui/react-aspect-ratio": "^1.1.7",
+    "@radix-ui/react-avatar": "^1.1.10",
+    "@radix-ui/react-checkbox": "^1.3.2",
+    "@radix-ui/react-collapsible": "^1.1.11",
+    "@radix-ui/react-context-menu": "^2.2.15",
+    "@radix-ui/react-dialog": "^1.1.14",
+    "@radix-ui/react-dropdown-menu": "^2.1.15",
+    "@radix-ui/react-hover-card": "^1.1.14",
+    "@radix-ui/react-label": "^2.1.7",
+    "@radix-ui/react-menubar": "^1.1.15",
+    "@radix-ui/react-navigation-menu": "^1.2.13",
+    "@radix-ui/react-popover": "^1.1.14",
+    "@radix-ui/react-progress": "^1.1.7",
+    "@radix-ui/react-radio-group": "^1.3.7",
+    "@radix-ui/react-scroll-area": "^1.2.9",
+    "@radix-ui/react-select": "^2.2.5",
+    "@radix-ui/react-separator": "^1.1.7",
+    "@radix-ui/react-slider": "^1.3.5",
+    "@radix-ui/react-slot": "^1.2.3",
+    "@radix-ui/react-switch": "^1.2.5",
+    "@radix-ui/react-tabs": "^1.1.12",
+    "@radix-ui/react-toggle": "^1.1.9",
+    "@radix-ui/react-toggle-group": "^1.1.10",
+    "@radix-ui/react-tooltip": "^1.2.7",
+    "@tailwindcss/vite": "^4.1.5",
+    "@tanstack/eslint-plugin-query": "^5.66.1",
+    "@tanstack/react-query": "^5.66.7",
+    "@tanstack/react-query-devtools": "^5.68.0",
+    "@tanstack/react-router": "^1.105.0",
+    "@tanstack/react-table": "^8.21.2",
+    "@tanstack/router-devtools": "^1.105.0",
+    "@tanstack/router-plugin": "^1.105.0",
+    "@tiptap/pm": "^2.11.5",
+    "@tiptap/react": "^2.11.5",
+    "@tiptap/starter-kit": "^2.11.5",
+    "@types/react": "^19.0.10",
+    "@types/react-dom": "^19.0.4",
+    "@types/react-grid-layout": "^1.3.5",
+    "@uidotdev/usehooks": "^2.4.1",
+    "@vendure/common": "3.3.7",
+    "@vendure/core": "3.3.7",
+    "@vitejs/plugin-react": "^4.3.4",
+    "acorn": "^8.11.3",
+    "acorn-walk": "^8.3.2",
+    "awesome-graphql-client": "^2.1.0",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "cmdk": "^1.1.1",
+    "date-fns": "^4.0.0",
+    "embla-carousel-react": "^8.6.0",
+    "express-rate-limit": "^7.5.0",
+    "fast-glob": "^3.3.2",
+    "fs-extra": "^11.2.0",
+    "gql.tada": "^1.8.10",
+    "graphql": "^16.10.0",
+    "input-otp": "^1.4.2",
+    "json-edit-react": "^1.23.1",
+    "lucide-react": "^0.475.0",
+    "motion": "^12.6.2",
+    "next-themes": "^0.4.6",
+    "react": "^19.0.0",
+    "react-day-picker": "^9.8.0",
+    "react-dom": "^19.0.0",
+    "react-dropzone": "^14.3.8",
+    "react-grid-layout": "^1.5.1",
+    "react-hook-form": "^7.60.0",
+    "react-resizable-panels": "^3.0.3",
+    "recharts": "^2.15.4",
+    "sonner": "^2.0.6",
+    "tailwind-merge": "^3.2.0",
+    "tailwindcss": "^4.1.5",
+    "tailwindcss-animate": "^1.0.7",
+    "tsconfig-paths": "^4.2.0",
+    "tw-animate-css": "^1.2.9",
+    "vaul": "^1.1.2",
+    "vite": "^6.3.5",
+    "zod": "^3.25.76"
+  },
+  "devDependencies": {
+    "@eslint/js": "^9.19.0",
+    "@types/node": "^22.13.4",
+    "eslint": "^9.19.0",
+    "eslint-plugin-react": "^7.37.4",
+    "eslint-plugin-react-hooks": "^5.0.0",
+    "eslint-plugin-react-refresh": "^0.4.18",
+    "globals": "^15.14.0",
+    "vite-plugin-dts": "^4.5.3"
+  },
+  "optionalDependencies": {
+    "lightningcss-linux-arm64-musl": "^1.29.3",
+    "lightningcss-linux-x64-musl": "^1.29.1"
+  }
 }

+ 33 - 0
packages/dashboard/plugin/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/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.js';
+import { MetricSummary, MetricSummaryInput } from '../types.js';
+
+@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);
+    }
+}

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

@@ -0,0 +1,88 @@
+import { RequestContext } from '@vendure/core';
+
+import { MetricData } from '../service/metrics.service.js';
+import { MetricInterval, MetricSummaryEntry, MetricType } from '../types.js';
+
+/**
+ * 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/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'];

+ 186 - 0
packages/dashboard/plugin/dashboard.plugin.ts

@@ -0,0 +1,186 @@
+import { MiddlewareConsumer, NestModule } from '@nestjs/common';
+import { Type } from '@vendure/common/lib/shared-types';
+import {
+    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.js';
+import { MetricsResolver } from './api/metrics.resolver.js';
+import { DEFAULT_APP_PATH, loggerCtx } from './constants.js';
+import { MetricsService } from './service/metrics.service.js';
+
+/**
+ * @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.
+     */
+    appDir: 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
+ *
+ * `npm install \@vendure/dashboard`
+ *
+ * ## Usage
+ *
+ * First you need to set up compilation of the Dashboard, using the Vite configuration
+ * described in the [Dashboard Getting Started Guide](/guides/extending-the-dashboard/getting-started/)
+ *
+ * Once set up, you run `npx vite build` to build the production version of the dashboard app.
+ *
+ * The built app files will be output to the location specified by `build.outDir` in your Vite
+ * config file. This should then be passed to the `appDir` init option, as in the example below:
+ *
+ * @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',
+ *       appDir: './dist/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 readonly 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, appDir } = DashboardPlugin.options;
+        const dashboardPath = appDir || 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;
+    }
+}

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

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

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

@@ -0,0 +1,3 @@
+{
+  "type": "commonjs"
+}

+ 175 - 0
packages/dashboard/plugin/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.js';
+import { loggerCtx } from '../constants.js';
+import { MetricInterval, MetricSummary, MetricSummaryEntry, MetricSummaryInput } from '../types.js';
+
+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 * 2 }); // 2 hours
+        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/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;
+}

+ 40 - 0
packages/dashboard/scripts/build-plugin.js

@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+import path from 'path';
+import process from 'process';
+import { fileURLToPath } from 'url';
+
+/**
+ * Build script for the dashboard plugin
+ * Copies package.json from plugin source to dist/plugin output directory
+ */
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const sourcePackageJson = path.join(__dirname, '../plugin/package.json');
+const outputDir = path.join(__dirname, '../dist/plugin');
+const outputPackageJson = path.join(outputDir, 'package.json');
+
+try {
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+        fs.mkdirSync(outputDir, { recursive: true });
+        console.log(`Created output directory: ${outputDir}`);
+    }
+
+    // Copy package.json
+    if (fs.existsSync(sourcePackageJson)) {
+        fs.copyFileSync(sourcePackageJson, outputPackageJson);
+        console.log(`Copied package.json from ${sourcePackageJson} to ${outputPackageJson}`);
+    } else {
+        console.error(`Source package.json not found at: ${sourcePackageJson}`);
+        process.exit(1);
+    }
+
+    console.log('Plugin build completed successfully');
+} catch (error) {
+    console.error('Plugin build failed:', error.message);
+    process.exit(1);
+}

+ 2 - 10
packages/dashboard/tsconfig.plugin.json

@@ -1,20 +1,12 @@
 {
   "extends": [
-    "./tsconfig.json"
+    "../../tsconfig.json"
   ],
   "compilerOptions": {
-    "module": "NodeNext",
-    "moduleResolution": "nodenext",
-    "resolveJsonModule": false,
-    "skipLibCheck": true,
-    "plugins": [],
     "declaration": true,
     "outDir": "./dist/plugin"
   },
   "include": [
-    "vite/**/*"
-  ],
-  "exclude": [
-    "vite/tests/**/*"
+    "plugin/**/*"
   ]
 }

+ 20 - 0
packages/dashboard/tsconfig.vite.json

@@ -0,0 +1,20 @@
+{
+  "extends": [
+    "./tsconfig.json"
+  ],
+  "compilerOptions": {
+    "module": "NodeNext",
+    "moduleResolution": "nodenext",
+    "resolveJsonModule": false,
+    "skipLibCheck": true,
+    "plugins": [],
+    "declaration": true,
+    "outDir": "./dist/vite"
+  },
+  "include": [
+    "vite/**/*"
+  ],
+  "exclude": [
+    "vite/tests/**/*"
+  ]
+}

+ 0 - 53
packages/dashboard/vite.config.lib.mts

@@ -1,53 +0,0 @@
-import { lingui } from '@lingui/vite-plugin';
-import react from '@vitejs/plugin-react';
-import path from 'path';
-import { pathToFileURL } from 'url';
-import { defineConfig } from 'vite';
-import dts from 'vite-plugin-dts';
-import { adminApiSchemaPlugin } from './vite/vite-plugin-admin-api-schema.js';
-import { configLoaderPlugin } from './vite/vite-plugin-config-loader.js';
-import { dashboardMetadataPlugin } from './vite/vite-plugin-dashboard-metadata.js';
-import { uiConfigPlugin } from './vite/vite-plugin-ui-config.js';
-import { getNormalizedVendureConfigPath } from './vite/vite-plugin-vendure-dashboard.js';
-
-const tempDir = path.join(import.meta.dirname, './.vendure-dashboard-temp');
-const normalizedVendureConfigPath = getNormalizedVendureConfigPath(
-    pathToFileURL('./sample-vendure-config.ts'),
-);
-
-/**
- * This config file is for building the dashboard library (the shared components, hooks etc).
- */
-export default defineConfig({
-    build: {
-        lib: {
-            entry: path.resolve(__dirname, 'src/lib/index.ts'),
-            formats: ['es'],
-        },
-        rollupOptions: {
-            external: ['react', 'react/jsx-runtime', 'react-dom', 'lucide-react', '@lingui/react'],
-        },
-        outDir: path.resolve(__dirname, 'dist', 'lib'),
-        sourcemap: true,
-    },
-    resolve: {
-        alias: {
-            '@': path.resolve(__dirname, './src/lib'),
-        },
-    },
-    plugins: [
-        lingui(),
-        react({
-            babel: {
-                plugins: ['@lingui/babel-plugin-lingui-macro'],
-            },
-        }),
-        configLoaderPlugin({ vendureConfigPath: normalizedVendureConfigPath, tempDir }),
-        adminApiSchemaPlugin(),
-        dashboardMetadataPlugin(),
-        uiConfigPlugin({ adminUiConfig: {} }),
-        dts({
-            include: ['src/lib/**/*.ts', 'src/lib/**/*.tsx'],
-        }),
-    ],
-});

+ 1 - 6
packages/dashboard/vite.config.mts

@@ -15,12 +15,6 @@ export default ({ mode }: { mode: string }) => {
 
     process.env.IS_LOCAL_DEV = adminApiHost.includes('localhost') ? 'true' : 'false';
 
-    console.log('Admin API Connection Info', {
-        adminApiHost,
-        adminApiPort,
-        isLocalDev: process.env.IS_LOCAL_DEV,
-    });
-
     const vendureConfigPath = process.env.VITEST
         ? // This should always be used for running the tests
           './sample-vendure-config.ts'
@@ -31,6 +25,7 @@ export default ({ mode }: { mode: string }) => {
         test: {
             globals: true,
             environment: 'jsdom',
+            exclude: ['./plugin/**/*', '**/node_modules/**/*'],
         },
         plugins: [
             vendureDashboardPlugin({

+ 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'),
         }),
     ],
 };

Fichier diff supprimé car celui-ci est trop grand
+ 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",

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

@@ -1,4 +1,4 @@
-import { vendureDashboardPlugin } from '@vendure/dashboard/plugin';
+import { vendureDashboardPlugin } from '@vendure/dashboard/vite';
 import path from 'path';
 import { pathToFileURL } from 'url';
 import { defineConfig } from 'vite';
@@ -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,
+        }),
     ],
 });

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff