diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 4170abd9..de3dc038 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -49,11 +49,17 @@ pnpm fetch-api-types # regenerate API types from server (must be running on :80 - `core/page-components/` — actual page UI logic lives here - `core/layouts/` — app shell, sidebar navigation -### Evaluator forms (`core/page-components/agent-detail/edit-control/evaluators/`) +### Evaluator forms (`core/evaluators/`) - Each evaluator type has its own folder: `json/`, `sql/`, `regex/`, `list/`, `luna2/` - Each folder exports: `form.tsx` (React component), `types.ts` (form types), `index.ts` (re-exports) - Registry in `evaluators/index.ts` maps evaluator names to form components +### Form guidelines (control definition + evaluator forms) +- **Always use the input's `label` prop** — never render a separate `` above the input as the label. Use Mantine's built-in `label` so required asterisks and layout are consistent. +- **Label with tooltip**: Use `LabelWithTooltip` from `@/core/components/label-with-tooltip` when a field needs an (i) icon that shows help text on hover. Pass `label={}` and, for inputs that support it, `labelProps={labelPropsInline}` so the label renders inline. +- **Required fields**: Use the input's `required` prop (e.g. Select, TextInput) so Mantine renders the red asterisk. Use `labelPropsInline` from the same module when you need the label inline. +- Applies to: control definition form (`edit-control/control-definition-form.tsx`) and all evaluator forms (`core/evaluators/*/form.tsx`). + ### Reusable components (`core/components/`) - Create reusable components that encapsulate common patterns and logic - **Best practice**: When creating wrapper components around Mantine components, extend the underlying component's props using `Omit` to exclude overridden props, then spread `...rest` to forward all other props @@ -96,9 +102,9 @@ export function SearchInput({ ## Common changes ### Add a new evaluator form -1. Create folder in `core/page-components/agent-detail/edit-control/evaluators//` +1. Create folder in `core/evaluators//` 2. Add `types.ts` with form field types -3. Add `form.tsx` with the form component (use Mantine form components) +3. Add `form.tsx` with the form component — use Mantine form components with `label` prop and `LabelWithTooltip` from `@/core/components/label-with-tooltip` for fields that need a tooltip (see Form guidelines above) 4. Add `index.ts` re-exporting form and types 5. Register in `evaluators/index.ts` diff --git a/ui/empty-css-module.js b/ui/empty-css-module.js new file mode 100644 index 00000000..69b4152a --- /dev/null +++ b/ui/empty-css-module.js @@ -0,0 +1,3 @@ +// Empty module to replace CSS imports from Jupiter DS +// (we import the CSS manually in _app.tsx) +module.exports = {}; diff --git a/ui/next.config.ts b/ui/next.config.ts index d2478502..59f1ebe3 100644 --- a/ui/next.config.ts +++ b/ui/next.config.ts @@ -4,6 +4,30 @@ const nextConfig: NextConfig = { pageExtensions: ['tsx', 'ts', 'jsx', 'js'], reactStrictMode: true, + // Transpile Jupiter DS package to handle CSS imports + transpilePackages: ['@rungalileo/jupiter-ds'], + + // Configure webpack to ignore CSS imports from Jupiter DS + // (we import the CSS manually in _app.tsx) + webpack: (config) => { + const webpack = require('webpack'); + + // Replace CSS imports from Jupiter DS with empty module + config.plugins.push( + new webpack.NormalModuleReplacementPlugin( + /^@mantine\/dates\/styles\.css$/, + (resource: any) => { + // Only replace if imported from Jupiter DS + if (resource.context && resource.context.includes('@rungalileo/jupiter-ds')) { + resource.request = require.resolve('./empty-css-module.js'); + } + } + ) + ); + + return config; + }, + // Optimize for CI/test builds ...(process.env.CI && { // Disable source maps in CI (faster builds, not needed for tests) diff --git a/ui/package.json b/ui/package.json index 09630ff1..78233193 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,8 @@ "test:integration:report": "playwright show-report" }, "dependencies": { + "@emotion/is-prop-valid": "^1.4.0", + "@mantine/charts": "^7.17.8", "@mantine/code-highlight": "7.17.5", "@mantine/core": "7.17.5", "@mantine/dates": "7.17.5", @@ -26,7 +28,7 @@ "@mantine/modals": "7.17.7", "@mantine/notifications": "7.17.7", "@rungalileo/icons": "^0.0.1", - "@rungalileo/jupiter-ds": "^0.0.2", + "@rungalileo/jupiter-ds": "^0.0.7", "@tabler/icons-react": "3.31", "@tanstack/react-query": "5.74.4", "@tanstack/react-query-devtools": "5.72.2", @@ -38,7 +40,8 @@ "next": "15.4.10", "openapi-fetch": "0.14.0", "react": "^19.1.4", - "react-dom": "^19.1.4" + "react-dom": "^19.1.4", + "recharts": "2" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 12501b16..ee4a2f6d 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@emotion/is-prop-valid': + specifier: ^1.4.0 + version: 1.4.0 + '@mantine/charts': + specifier: ^7.17.8 + version: 7.17.8(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(recharts@2.15.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4)) '@mantine/code-highlight': specifier: 7.17.5 version: 7.17.5(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4) @@ -36,8 +42,8 @@ importers: specifier: ^0.0.1 version: 0.0.1(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4) '@rungalileo/jupiter-ds': - specifier: ^0.0.2 - version: 0.0.2(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + specifier: ^0.0.7 + version: 0.0.7(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/dates@7.17.5(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(dayjs@1.11.19)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(@tabler/icons-react@3.31.0(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4) '@tabler/icons-react': specifier: '3.31' version: 3.31.0(react@19.1.4) @@ -74,6 +80,9 @@ importers: react-dom: specifier: ^19.1.4 version: 19.1.4(react@19.1.4) + recharts: + specifier: '2' + version: 2.15.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4) devDependencies: '@eslint/eslintrc': specifier: ^3.3.3 @@ -207,6 +216,12 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -435,6 +450,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mantine/charts@7.17.8': + resolution: {integrity: sha512-lzDa2JM0uD2X32vnUPtERJc4V5nYkrbpOpnC/G3p0Kkwcxh9v59p5uMDxHXoHcv/OsMPALKYWBkY9aGWvD/E4g==} + peerDependencies: + '@mantine/core': 7.17.8 + '@mantine/hooks': 7.17.8 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + recharts: ^2.13.3 + '@mantine/code-highlight@7.17.5': resolution: {integrity: sha512-EzOLEGSbc3Psp/dfpf9yakiWEhcMPZ8qsCuSWvEVJIC40N4VPQ7Pdz1tyN2NSI9Qa31BGzHcqXZcxVtfZ0yG5A==} peerDependencies: @@ -596,11 +620,13 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@rungalileo/jupiter-ds@0.0.2': - resolution: {integrity: sha512-OCzGt4iXMYzsll0ADn/4LoN++rsH8Rd2w2jmznJv+BJ7+CN902LTn42CWxh1NqYA1SWn++a57QaZaSuj8MkOjw==} + '@rungalileo/jupiter-ds@0.0.7': + resolution: {integrity: sha512-edBB7pw62+dKZKtDd3duk1uZ2NQD0iEfl9mQEVjsC+th5ORUpQDKNrgIccC3311e8VBlWYDwBx68FjfwySkwKg==} peerDependencies: '@mantine/core': ^7.17.0 + '@mantine/dates': ^7.17.0 '@mantine/hooks': ^7.17.0 + '@tabler/icons-react': ^3.0.0 react: '>=18.0.0' react-dom: '>=18.0.0' @@ -734,6 +760,33 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1091,6 +1144,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1129,6 +1226,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1342,9 +1442,16 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -1544,6 +1651,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -1802,6 +1913,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2072,6 +2186,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-number-format@5.4.4: resolution: {integrity: sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==} peerDependencies: @@ -2098,6 +2215,12 @@ packages: '@types/react': optional: true + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -2124,6 +2247,16 @@ packages: resolution: {integrity: sha512-DHINL3PAmPUiK1uszfbKiXqfE03eszdt5BpVSuEAHb5nfmNPwnsy7g39h2t8aXFc/Bv99GH81s+j8dobtD+jOw==} engines: {node: '>=0.10.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2306,6 +2439,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2431,6 +2567,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2601,6 +2740,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -2799,6 +2944,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mantine/charts@7.17.8(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)(recharts@2.15.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4))': + dependencies: + '@mantine/core': 7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + '@mantine/hooks': 7.17.5(react@19.1.4) + react: 19.1.4 + react-dom: 19.1.4(react@19.1.4) + recharts: 2.15.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + '@mantine/code-highlight@7.17.5(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)': dependencies: '@mantine/core': 7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) @@ -2955,10 +3108,12 @@ snapshots: react: 19.1.4 react-dom: 19.1.4(react@19.1.4) - '@rungalileo/jupiter-ds@0.0.2(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)': + '@rungalileo/jupiter-ds@0.0.7(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/dates@7.17.5(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(dayjs@1.11.19)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(@tabler/icons-react@3.31.0(react@19.1.4))(react-dom@19.1.4(react@19.1.4))(react@19.1.4)': dependencies: '@mantine/core': 7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + '@mantine/dates': 7.17.5(@mantine/core@7.17.5(@mantine/hooks@7.17.5(react@19.1.4))(@types/react@19.2.7)(react-dom@19.1.4(react@19.1.4))(react@19.1.4))(@mantine/hooks@7.17.5(react@19.1.4))(dayjs@1.11.19)(react-dom@19.1.4(react@19.1.4))(react@19.1.4) '@mantine/hooks': 7.17.5(react@19.1.4) + '@tabler/icons-react': 3.31.0(react@19.1.4) clsx: 2.1.1 react: 19.1.4 react-dom: 19.1.4(react@19.1.4) @@ -3071,6 +3226,30 @@ snapshots: tslib: 2.8.1 optional: true + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3440,6 +3619,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -3474,6 +3691,8 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3836,8 +4055,12 @@ snapshots: esutils@2.0.3: {} + eventemitter3@4.0.7: {} + fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4029,6 +4252,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -4265,6 +4490,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.23: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -4530,6 +4757,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-number-format@5.4.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4): dependencies: react: 19.1.4 @@ -4554,6 +4783,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-smooth@4.0.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 19.1.4 + react-dom: 19.1.4(react@19.1.4) + react-transition-group: 4.4.5(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.1.4): dependencies: get-nonce: 1.0.1 @@ -4582,6 +4819,23 @@ snapshots: react@19.1.4: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.23 + react: 19.1.4 + react-dom: 19.1.4(react@19.1.4) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.1.4(react@19.1.4))(react@19.1.4) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4827,6 +5081,8 @@ snapshots: tapable@2.3.0: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -4980,6 +5236,23 @@ snapshots: util-deprecate@1.0.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index 850330ae..504bc774 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -116,8 +116,9 @@ export const api = { observability: { getStats: (params: { agent_uuid: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d"; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; control_id?: number | null; + include_timeseries?: boolean; }) => apiClient.GET("/api/v1/observability/stats", { params: { query: params }, diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index 010b5158..ccd37b96 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -21,6 +21,7 @@ export interface paths { * Args: * cursor: Optional cursor for pagination (UUID of last agent from previous page) * limit: Pagination limit (default 20, max 100) + * name: Optional name filter (case-insensitive partial match) * db: Database session (injected) * * Returns: @@ -783,19 +784,19 @@ export interface paths { }; /** * Get Stats - * @description Get aggregated control execution statistics. + * @description Get agent-level aggregated statistics. * - * Statistics are computed at query time from raw events. This is fast - * enough for most use cases (sub-200ms for 1-hour windows). + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. * * Args: * agent_uuid: Agent to get stats for - * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d) - * control_id: Optional filter by specific control + * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) + * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) * * Returns: - * StatsResponse with per-control statistics + * StatsResponse with agent-level totals and per-control breakdown */ get: operations["get_stats_api_v1_observability_stats_get"]; put?: never; @@ -806,6 +807,38 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/observability/stats/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID to get stats for + * agent_uuid: Agent to get stats for + * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) + * include_timeseries: Include time-series data points for trend visualization + * store: Event store (injected) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/observability/status": { parameters: { query?: never; @@ -922,6 +955,22 @@ export interface components { */ controls: components["schemas"]["Control"][]; }; + /** + * AgentRef + * @description Reference to an agent (for listing which agents use a control). + */ + AgentRef: { + /** + * Agent Id + * @description Agent UUID + */ + agent_id: string; + /** + * Agent Name + * @description Agent name + */ + agent_name: string; + }; /** * AgentSummary * @description Summary of an agent for list responses. @@ -1478,6 +1527,44 @@ export interface components { */ avg_duration_ms?: number | null; }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_uuid: Agent UUID + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Uuid + * Format: uuid + * @description Agent UUID + */ + agent_uuid: string; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + control_name: string; + /** @description Control statistics */ + stats: components["schemas"]["StatsTotals"]; + }; /** * ControlSummary * @description Summary of a control for list responses. @@ -1524,6 +1611,8 @@ export interface components { * @description Control tags */ tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components["schemas"]["AgentRef"] | null; }; /** CreateControlRequest */ CreateControlRequest: { @@ -2422,22 +2511,15 @@ export interface components { }; /** * StatsResponse - * @description Response model for aggregated statistics. + * @description Response model for agent-level aggregated statistics. * - * Invariant: total_executions = total_matches + total_non_matches + total_errors - * - * Matches have actions (allow, deny, warn, log) tracked in action_counts. - * sum(action_counts.values()) == total_matches + * Contains agent-level totals (with optional timeseries) and per-control breakdown. * * Attributes: * agent_uuid: Agent UUID * time_range: Time range used - * stats: List of per-control statistics - * total_executions: Total executions across all controls - * total_matches: Total matches across all controls (evaluator matched) - * total_non_matches: Total non-matches across all controls (evaluator didn't match) - * total_errors: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions + * totals: Agent-level aggregate statistics (includes timeseries) + * controls: Per-control breakdown for discovery and detail */ StatsResponse: { /** @@ -2451,34 +2533,55 @@ export interface components { * @description Time range used */ time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components["schemas"]["StatsTotals"]; /** - * Stats - * @description Per-control statistics + * Controls + * @description Per-control breakdown */ - stats: components["schemas"]["ControlStats"][]; + controls: components["schemas"]["ControlStats"][]; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * timeseries: Time-series data points (only when include_timeseries=true) + */ + StatsTotals: { /** - * Total Executions - * @description Total executions across all controls + * Execution Count + * @description Total executions */ - total_executions: number; + execution_count: number; /** - * Total Matches - * @description Total matches across all controls + * Match Count + * @description Total matches * @default 0 */ - total_matches: number; + match_count: number; /** - * Total Non Matches - * @description Total non-matches across all controls + * Non Match Count + * @description Total non-matches * @default 0 */ - total_non_matches: number; + non_match_count: number; /** - * Total Errors - * @description Total errors across all controls + * Error Count + * @description Total errors * @default 0 */ - total_errors: number; + error_count: number; /** * Action Counts * @description Action breakdown for matches: {allow, deny, warn, log} @@ -2486,6 +2589,11 @@ export interface components { action_counts?: { [key: string]: number; }; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components["schemas"]["TimeseriesBucket"][] | null; }; /** * Step @@ -2604,6 +2712,67 @@ export interface components { [key: string]: unknown; } | null; }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + /** + * Execution Count + * @description Total executions in bucket + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Error Count + * @description Errors in bucket + */ + error_count: number; + /** + * Action Counts + * @description Action breakdown: {allow, deny, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Avg Confidence + * @description Average confidence score + */ + avg_confidence?: number | null; + /** + * Avg Duration Ms + * @description Average duration (ms) + */ + avg_duration_ms?: number | null; + }; /** * UpdateEvaluatorConfigRequest * @description Request to replace an evaluator config template. @@ -2655,6 +2824,7 @@ export interface operations { query?: { cursor?: string | null; limit?: number; + name?: string | null; }; header?: never; path?: never; @@ -3639,8 +3809,8 @@ export interface operations { parameters: { query: { agent_uuid: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d"; - control_id?: number | null; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; }; header?: never; path?: never; @@ -3668,6 +3838,41 @@ export interface operations { }; }; }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_uuid: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ControlStatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_status_api_v1_observability_status_get: { parameters: { query?: never; diff --git a/ui/src/core/components/label-with-tooltip.tsx b/ui/src/core/components/label-with-tooltip.tsx new file mode 100644 index 00000000..a7f8cd8d --- /dev/null +++ b/ui/src/core/components/label-with-tooltip.tsx @@ -0,0 +1,30 @@ +import { Group, Text, Tooltip } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; + +export interface LabelWithTooltipProps { + label: string; + tooltip: string; +} + +/** + * Label with (i) icon that shows tooltip on hover. + * Use as the `label` prop of Mantine form inputs (Select, TextInput, etc.) + * so the input always uses the label prop and optional tooltip is consistent. + */ +export function LabelWithTooltip({ label, tooltip }: LabelWithTooltipProps) { + return ( + + + {label} + + + + + + ); +} + +/** Pass to labelProps on inputs so the label renders inline. */ +export const labelPropsInline = { + style: { display: "inline-block" as const }, +}; diff --git a/ui/src/core/evaluators/json/form.tsx b/ui/src/core/evaluators/json/form.tsx index ec2fbf31..ae7c9e92 100644 --- a/ui/src/core/evaluators/json/form.tsx +++ b/ui/src/core/evaluators/json/form.tsx @@ -1,180 +1,177 @@ import { - Box, Checkbox, Divider, Select, Stack, - Text, Textarea, TextInput, } from "@mantine/core"; +import { + labelPropsInline, + LabelWithTooltip, +} from "@/core/components/label-with-tooltip"; + import type { EvaluatorFormProps } from "../types"; import type { JsonFormValues } from "./types"; export const JsonForm = ({ form }: EvaluatorFormProps) => { return ( - - - - - - JSON Schema - -