From 54d3eaaa02407f5b404f9be3d0023247835343cd Mon Sep 17 00:00:00 2001 From: Anthony Ly Date: Fri, 28 Jun 2024 12:16:05 +0200 Subject: [PATCH] feat: create analysis html report (#307) --- cli/.gitignore | 1 + cli/.npmignore | 1 + cli/html-report/.eslintrc.cjs | 16 + cli/html-report/.gitignore | 24 + cli/html-report/README.md | 39 + cli/html-report/app/globals.css | 76 + cli/html-report/components.json | 17 + cli/html-report/jest.config.ts | 13 + cli/html-report/package.json | 55 + cli/html-report/postcss.config.js | 6 + cli/html-report/report.html | 16 + cli/html-report/setup-test.ts | 22 + cli/html-report/src/App.tsx | 50 + .../src/components/azimutt/Logo/Logo.tsx | 14 + .../src/components/layout/AppBar/AppBar.tsx | 13 + .../layout/MainLayout/MainLayout.test.tsx | 14 + .../layout/MainLayout/MainLayout.tsx | 18 + .../ReportFilters/ReportFilters.test.tsx | 15 + .../report/ReportFilters/ReportFilters.tsx | 41 + .../ReportCategoryFilter.test.tsx | 9 + .../ReportCategoryFilter.tsx | 35 + .../ReportRuleFilter.test.tsx | 10 + .../ReportRuleFilter/ReportRuleFilter.tsx | 22 + .../ReportSeverityFilter.test.tsx | 9 + .../ReportSeverityFilter.tsx | 23 + .../ReportTableFilter.test.tsx | 10 + .../ReportTableFilter/ReportTableFilter.tsx | 22 + .../ReportStats/ReportStatCell.test.tsx | 16 + .../report/ReportStats/ReportStatCell.tsx | 15 + .../report/ReportStats/ReportStats.test.tsx | 30 + .../report/ReportStats/ReportStats.tsx | 19 + .../ReportStats/ReportStatsGrid.test.tsx | 72 + .../report/ReportStats/ReportStatsGrid.tsx | 57 + .../ViolationsList/ViolationList.test.tsx | 75 + .../ViolationsList/ViolationListItem.test.tsx | 117 + .../report/ViolationsList/ViolationsList.tsx | 22 + .../ViolationsList/ViolationsListItem.tsx | 50 + cli/html-report/src/components/ui/badge.tsx | 36 + cli/html-report/src/components/ui/button.tsx | 56 + cli/html-report/src/components/ui/card.tsx | 79 + cli/html-report/src/components/ui/command.tsx | 153 ++ cli/html-report/src/components/ui/dialog.tsx | 120 + .../src/components/ui/multi-select.tsx | 330 +++ cli/html-report/src/components/ui/popover.tsx | 29 + cli/html-report/src/components/ui/select.tsx | 158 ++ .../src/components/ui/separator.tsx | 29 + cli/html-report/src/components/ui/table.tsx | 117 + .../src/constants/env.constants.ts | 3 + .../src/constants/report.constants.ts | 1070 +++++++++ cli/html-report/src/context/ReportContext.tsx | 19 + .../src/context/reportContextTestTool.ts | 22 + cli/html-report/src/hooks/useReport.test.tsx | 213 ++ cli/html-report/src/hooks/useReport.tsx | 98 + cli/html-report/src/lib/utils.ts | 38 + cli/html-report/src/main.tsx | 10 + .../src/test/__ mocks __/fileMock.js | 4 + cli/html-report/src/vite-env.d.ts | 1 + cli/html-report/tailwind.config.js | 77 + cli/html-report/tsconfig.json | 31 + cli/html-report/tsconfig.node.json | 11 + cli/html-report/vite.config.ts | 24 + cli/jest.config.js | 3 +- cli/src/analyze.ts | 705 ++++-- cli/src/index.ts | 1 + cli/src/utils/file.ts | 6 + gateway/src/services/tracking.ts | 2 +- libs/models/src/analyze/rule.ts | 121 +- pnpm-lock.yaml | 2085 ++++++++++++++--- pnpm-workspace.yaml | 1 + 69 files changed, 6196 insertions(+), 520 deletions(-) create mode 100644 cli/html-report/.eslintrc.cjs create mode 100644 cli/html-report/.gitignore create mode 100644 cli/html-report/README.md create mode 100644 cli/html-report/app/globals.css create mode 100644 cli/html-report/components.json create mode 100644 cli/html-report/jest.config.ts create mode 100644 cli/html-report/package.json create mode 100644 cli/html-report/postcss.config.js create mode 100644 cli/html-report/report.html create mode 100644 cli/html-report/setup-test.ts create mode 100644 cli/html-report/src/App.tsx create mode 100644 cli/html-report/src/components/azimutt/Logo/Logo.tsx create mode 100644 cli/html-report/src/components/layout/AppBar/AppBar.tsx create mode 100644 cli/html-report/src/components/layout/MainLayout/MainLayout.test.tsx create mode 100644 cli/html-report/src/components/layout/MainLayout/MainLayout.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/ReportFilters.test.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/ReportFilters.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.test.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.test.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.test.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.test.tsx create mode 100644 cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStatCell.test.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStatCell.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStats.test.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStats.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStatsGrid.test.tsx create mode 100644 cli/html-report/src/components/report/ReportStats/ReportStatsGrid.tsx create mode 100644 cli/html-report/src/components/report/ViolationsList/ViolationList.test.tsx create mode 100644 cli/html-report/src/components/report/ViolationsList/ViolationListItem.test.tsx create mode 100644 cli/html-report/src/components/report/ViolationsList/ViolationsList.tsx create mode 100644 cli/html-report/src/components/report/ViolationsList/ViolationsListItem.tsx create mode 100644 cli/html-report/src/components/ui/badge.tsx create mode 100644 cli/html-report/src/components/ui/button.tsx create mode 100644 cli/html-report/src/components/ui/card.tsx create mode 100644 cli/html-report/src/components/ui/command.tsx create mode 100644 cli/html-report/src/components/ui/dialog.tsx create mode 100644 cli/html-report/src/components/ui/multi-select.tsx create mode 100644 cli/html-report/src/components/ui/popover.tsx create mode 100644 cli/html-report/src/components/ui/select.tsx create mode 100644 cli/html-report/src/components/ui/separator.tsx create mode 100644 cli/html-report/src/components/ui/table.tsx create mode 100644 cli/html-report/src/constants/env.constants.ts create mode 100644 cli/html-report/src/constants/report.constants.ts create mode 100644 cli/html-report/src/context/ReportContext.tsx create mode 100644 cli/html-report/src/context/reportContextTestTool.ts create mode 100644 cli/html-report/src/hooks/useReport.test.tsx create mode 100644 cli/html-report/src/hooks/useReport.tsx create mode 100644 cli/html-report/src/lib/utils.ts create mode 100644 cli/html-report/src/main.tsx create mode 100644 cli/html-report/src/test/__ mocks __/fileMock.js create mode 100644 cli/html-report/src/vite-env.d.ts create mode 100644 cli/html-report/tailwind.config.js create mode 100644 cli/html-report/tsconfig.json create mode 100644 cli/html-report/tsconfig.node.json create mode 100644 cli/html-report/vite.config.ts diff --git a/cli/.gitignore b/cli/.gitignore index 173047170..88d431a13 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -3,3 +3,4 @@ out local src/**/*.js *.tgz +resources/report.html \ No newline at end of file diff --git a/cli/.npmignore b/cli/.npmignore index ceefdda91..6bf866bef 100644 --- a/cli/.npmignore +++ b/cli/.npmignore @@ -5,3 +5,4 @@ src/*.test.ts jest.config.js tsconfig.json *.tgz +html-report \ No newline at end of file diff --git a/cli/html-report/.eslintrc.cjs b/cli/html-report/.eslintrc.cjs new file mode 100644 index 000000000..128526a52 --- /dev/null +++ b/cli/html-report/.eslintrc.cjs @@ -0,0 +1,16 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + ], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "@typescript-eslint/ban-ts-comment": "off", + "react-refresh/only-export-components": "off", + }, +} diff --git a/cli/html-report/.gitignore b/cli/html-report/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/cli/html-report/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/cli/html-report/README.md b/cli/html-report/README.md new file mode 100644 index 000000000..e22d0017a --- /dev/null +++ b/cli/html-report/README.md @@ -0,0 +1,39 @@ +# Azimutt Html Report + +Azimutt Html Report generates the html template file for the `analyze cli` + +## Developing + +The project uses ViteJS + React. + +Start by installing dependencies + +```bash +pnpm install +``` + +Launch developer mode to visualize changes with hot reload + +```bash +pnpm run dev +``` + +Launch tests with + +```bash +pnpm run test +``` + +### Customize mock data + +In development mode, the app loads report data from the file `src/constants/report.constants.ts` + +## Publish + +Build the react app + +```bash +pnpm run build +``` + +In production mode, the data is set in the global variable `__REPORT__` each time the `analyze cli` is called. diff --git a/cli/html-report/app/globals.css b/cli/html-report/app/globals.css new file mode 100644 index 000000000..8abdb15c9 --- /dev/null +++ b/cli/html-report/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/cli/html-report/components.json b/cli/html-report/components.json new file mode 100644 index 000000000..da9f8d1d6 --- /dev/null +++ b/cli/html-report/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/cli/html-report/jest.config.ts b/cli/html-report/jest.config.ts new file mode 100644 index 000000000..89b84b32f --- /dev/null +++ b/cli/html-report/jest.config.ts @@ -0,0 +1,13 @@ +export default { + preset: "ts-jest", + testEnvironment: "jest-environment-jsdom", + transform: { + "^.+\\.tsx?$": "ts-jest", + // process `*.tsx` files with `ts-jest` + }, + moduleNameMapper: { + "\\.(gif|ttf|eot|svg|png)$": "/test/__ mocks __/fileMock.js", + "@/(.*)": "/src/$1", + }, + setupFiles: ["./setup-test.ts"], +} diff --git a/cli/html-report/package.json b/cli/html-report/package.json new file mode 100644 index 000000000..f0017fe8e --- /dev/null +++ b/cli/html-report/package.json @@ -0,0 +1,55 @@ +{ + "name": "html-report", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "jest" + }, + "dependencies": { + "@azimutt/models": "workspace:^", + "@azimutt/utils": "workspace:^", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "jest-environment-jsdom": "^29.7.0", + "lucide-react": "^0.395.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jest": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "ts-jest": "^29.1.5", + "typescript": "^5.2.2", + "vite": "^5.2.0", + "vite-plugin-singlefile": "^2.0.1" + } +} diff --git a/cli/html-report/postcss.config.js b/cli/html-report/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/cli/html-report/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/cli/html-report/report.html b/cli/html-report/report.html new file mode 100644 index 000000000..56296acb3 --- /dev/null +++ b/cli/html-report/report.html @@ -0,0 +1,16 @@ + + + + + + + Azimutt Report + + + + +
+ + + + \ No newline at end of file diff --git a/cli/html-report/setup-test.ts b/cli/html-report/setup-test.ts new file mode 100644 index 000000000..a2b08a96c --- /dev/null +++ b/cli/html-report/setup-test.ts @@ -0,0 +1,22 @@ +jest.mock("./src/constants/env.constants.ts", () => ({ + ENVIRONMENT: "development", + PROD: false, +})) + +class MockPointerEvent extends Event { + button: number + ctrlKey: boolean + pointerType: string + + constructor(type: string, props: PointerEventInit) { + super(type, props) + this.button = props.button || 0 + this.ctrlKey = props.ctrlKey || false + this.pointerType = props.pointerType || "mouse" + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +window.PointerEvent = MockPointerEvent as any +window.HTMLElement.prototype.scrollIntoView = jest.fn() +window.HTMLElement.prototype.releasePointerCapture = jest.fn() diff --git a/cli/html-report/src/App.tsx b/cli/html-report/src/App.tsx new file mode 100644 index 000000000..70e6f9465 --- /dev/null +++ b/cli/html-report/src/App.tsx @@ -0,0 +1,50 @@ +import { REPORT } from "./constants/report.constants" +import { MainLayout } from "./components/layout/MainLayout/MainLayout" +import { ReportContext } from "./context/ReportContext" +import { RuleLevel } from "@azimutt/models" +import { useState } from "react" +import { ReportStats } from "./components/report/ReportStats/ReportStats" +import { ReportFilters } from "./components/report/ReportFilters/ReportFilters" +import { ViolationsList } from "./components/report/ViolationsList/ViolationsList" + +function App() { + const [selectedLevels, setSelectedLevels] = useState([]) + const [selectedCategories, setSelectedCategories] = useState([]) + const [selectedRules, setSelectedRules] = useState([]) + const [selectedTables, setSelectedTables] = useState([]) + + const handleChange = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (setter: React.Dispatch>) => (value: any[]) => { + setter(value) + } + + return ( + + +
+ + + +
+
+
+ ) +} + +export default App diff --git a/cli/html-report/src/components/azimutt/Logo/Logo.tsx b/cli/html-report/src/components/azimutt/Logo/Logo.tsx new file mode 100644 index 000000000..2c5c8de4c --- /dev/null +++ b/cli/html-report/src/components/azimutt/Logo/Logo.tsx @@ -0,0 +1,14 @@ +export interface LogoProps { + className?: string +} + +export const Logo = ({ className }: LogoProps) => { + return ( + Azimutt logo + ) +} diff --git a/cli/html-report/src/components/layout/AppBar/AppBar.tsx b/cli/html-report/src/components/layout/AppBar/AppBar.tsx new file mode 100644 index 000000000..246f81471 --- /dev/null +++ b/cli/html-report/src/components/layout/AppBar/AppBar.tsx @@ -0,0 +1,13 @@ +import { Logo } from "@/components/azimutt/Logo/Logo" + +export interface AppBarProps {} + +export const AppBar = () => { + return ( +
+ + + +
+ ) +} diff --git a/cli/html-report/src/components/layout/MainLayout/MainLayout.test.tsx b/cli/html-report/src/components/layout/MainLayout/MainLayout.test.tsx new file mode 100644 index 000000000..a01317358 --- /dev/null +++ b/cli/html-report/src/components/layout/MainLayout/MainLayout.test.tsx @@ -0,0 +1,14 @@ +import { render, screen } from "@testing-library/react" +import { MainLayout } from "./MainLayout" + +describe("MainLayout", () => { + test("Should render content", () => { + render( + +

Content

+
+ ) + + expect(screen.getByText("Content")).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/layout/MainLayout/MainLayout.tsx b/cli/html-report/src/components/layout/MainLayout/MainLayout.tsx new file mode 100644 index 000000000..d59a69bfa --- /dev/null +++ b/cli/html-report/src/components/layout/MainLayout/MainLayout.tsx @@ -0,0 +1,18 @@ +import { AppBar } from "../AppBar/AppBar" + +export interface MainLayoutProps { + children?: React.ReactNode +} + +export const MainLayout = ({ children }: MainLayoutProps) => { + return ( +
+ +
+
+
{children}
+
+
+
+ ) +} diff --git a/cli/html-report/src/components/report/ReportFilters/ReportFilters.test.tsx b/cli/html-report/src/components/report/ReportFilters/ReportFilters.test.tsx new file mode 100644 index 000000000..12147f2a6 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/ReportFilters.test.tsx @@ -0,0 +1,15 @@ +import { render } from "@testing-library/react" +import { ReportFilters } from "./ReportFilters" + +describe("ReportFilters", () => { + test("Should render", () => { + render( + + ) + }) +}) diff --git a/cli/html-report/src/components/report/ReportFilters/ReportFilters.tsx b/cli/html-report/src/components/report/ReportFilters/ReportFilters.tsx new file mode 100644 index 000000000..c606c2464 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/ReportFilters.tsx @@ -0,0 +1,41 @@ +import { useReport } from "@/hooks/useReport" +import { ReportCategoryFilter } from "./filters/ReportCategoryFilter/ReportCategoryFilter" +import { ReportRuleFilter } from "./filters/ReportRuleFilter/ReportRuleFilter" +import { ReportSeverityFilter } from "./filters/ReportSeverityFilter/ReportSeverityFilter" +import { ReportTableFilter } from "./filters/ReportTableFilter/ReportTableFilter" + +export interface ReportFiltersProps { + onSeveritiesChange: (severities: string[]) => void + onCategoriesChange: (categories: string[]) => void + onRulesChange: (rules: string[]) => void + onTablesChange: (tables: string[]) => void +} + +export const ReportFilters = ({ + onSeveritiesChange, + onCategoriesChange, + onRulesChange, + onTablesChange, +}: ReportFiltersProps) => { + const { filters, rules, tables } = useReport() + + return ( +
+ + + + +
+ ) +} diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.test.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.test.tsx new file mode 100644 index 000000000..48d66e326 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from "@testing-library/react" +import { ReportCategoryFilter } from "./ReportCategoryFilter" + +describe("ReportCategoryFilter", () => { + test("Should render", () => { + render() + expect(screen.getByText("Categories")).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.tsx new file mode 100644 index 000000000..ebc0e29e8 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportCategoryFilter/ReportCategoryFilter.tsx @@ -0,0 +1,35 @@ +import { MultiSelect } from "@/components/ui/multi-select" + +const CATEGORIES = [ + { + label: "Schema design", + value: "schema-design", + }, + { + label: "Performance", + value: "performance", + }, + { + label: "DB Health", + value: "db-health", + }, +] + +export interface ReportCategoryFilterProps { + selected?: string[] + onChange?: (value: string[]) => void +} + +export const ReportCategoryFilter = ({ + selected, + onChange, +}: ReportCategoryFilterProps) => { + return ( + onChange?.(value)} + placeholder="Categories" + /> + ) +} diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.test.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.test.tsx new file mode 100644 index 000000000..0c730f072 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.test.tsx @@ -0,0 +1,10 @@ +import { render, screen } from "@testing-library/react" +import { ReportRuleFilter } from "./ReportRuleFilter" + +describe("ReportRuleFilter", () => { + test("Should render", () => { + const rules = ["missing primary key"] + render() + expect(screen.getByText("Rules")).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.tsx new file mode 100644 index 000000000..b3f0227b7 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportRuleFilter/ReportRuleFilter.tsx @@ -0,0 +1,22 @@ +import { MultiSelect } from "@/components/ui/multi-select" + +export interface ReportRuleFilterProps { + rules: string[] + selected: string[] + onChange?: (values: string[]) => void +} + +export const ReportRuleFilter = ({ + rules, + selected, + onChange, +}: ReportRuleFilterProps) => { + return ( + ({ label: rule, value: rule }))} + defaultValue={selected} + onValueChange={(value) => onChange?.(value)} + placeholder="Rules" + /> + ) +} diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.test.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.test.tsx new file mode 100644 index 000000000..9e5698205 --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from "@testing-library/react" +import { ReportSeverityFilter } from "./ReportSeverityFilter" + +describe("ReportSeverityFilter", () => { + test("Should render", () => { + render() + expect(screen.getByText("Severity")).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.tsx new file mode 100644 index 000000000..85a32f0ce --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportSeverityFilter/ReportSeverityFilter.tsx @@ -0,0 +1,23 @@ +import { MultiSelect } from "@/components/ui/multi-select" +import { RuleLevel } from "@azimutt/models" + +const levels: RuleLevel[] = ["high", "medium", "low", "hint"] + +export interface ReportSeverityFilterProps { + selected?: string[] + onChange?: (value: string[]) => void +} + +export const ReportSeverityFilter = ({ + selected, + onChange, +}: ReportSeverityFilterProps) => { + return ( + ({ label: level, value: level }))} + onValueChange={(value) => onChange?.(value)} + defaultValue={selected ?? []} + placeholder="Severity" + /> + ) +} diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.test.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.test.tsx new file mode 100644 index 000000000..33a3473dd --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.test.tsx @@ -0,0 +1,10 @@ +import { render, screen } from "@testing-library/react" +import { ReportTableFilter } from "./ReportTableFilter" + +describe("ReportTableFilter", () => { + test("Should render", () => { + const tables = ["public.users"] + render() + expect(screen.getByText("Tables")).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.tsx b/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.tsx new file mode 100644 index 000000000..31a856dce --- /dev/null +++ b/cli/html-report/src/components/report/ReportFilters/filters/ReportTableFilter/ReportTableFilter.tsx @@ -0,0 +1,22 @@ +import { MultiSelect } from "@/components/ui/multi-select" + +export interface ReportTableFilterProps { + tables: string[] + selected: string[] + onChange?: (values: string[]) => void +} + +export const ReportTableFilter = ({ + tables, + selected, + onChange, +}: ReportTableFilterProps) => { + return ( + ({ label: table, value: table }))} + defaultValue={selected} + onValueChange={(value) => onChange?.(value)} + placeholder="Tables" + /> + ) +} diff --git a/cli/html-report/src/components/report/ReportStats/ReportStatCell.test.tsx b/cli/html-report/src/components/report/ReportStats/ReportStatCell.test.tsx new file mode 100644 index 000000000..6e225ae62 --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStatCell.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react" +import { ReportStatCell } from "./ReportStatCell" + +describe("ReportStatCell", () => { + test("Should render label", () => { + const given = "My label" + render() + expect(screen.getByText(given)).toBeDefined() + }) + + test("Should render value", () => { + const given = "603" + render() + expect(screen.getByText(given)).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportStats/ReportStatCell.tsx b/cli/html-report/src/components/report/ReportStats/ReportStatCell.tsx new file mode 100644 index 000000000..938541d0a --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStatCell.tsx @@ -0,0 +1,15 @@ +export interface ReportStatCellProps { + label: string + value: string +} + +export const ReportStatCell = ({ label, value }: ReportStatCellProps) => { + return ( +
+
{label}
+
+ {value} +
+
+ ) +} diff --git a/cli/html-report/src/components/report/ReportStats/ReportStats.test.tsx b/cli/html-report/src/components/report/ReportStats/ReportStats.test.tsx new file mode 100644 index 000000000..7beb68c1d --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStats.test.tsx @@ -0,0 +1,30 @@ +import { reportContextFactory } from "@/context/reportContextTestTool" +import * as ReportContext from "@/context/ReportContext" +import { ReportStats } from "./ReportStats" +import { render, screen } from "@testing-library/react" +import { AnalyzeReportHtmlStats } from "@azimutt/models" + +describe("ReportStats", () => { + test("Should render stats from context", () => { + const given: AnalyzeReportHtmlStats = { + nb_entities: 34, + nb_relations: 45, + nb_queries: 193, + nb_types: 46, + nb_rules: 29, + } + + const contextValues = reportContextFactory({ + stats: given, + }) + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + render() + expect(screen.getByText(given.nb_entities)).toBeDefined() + expect(screen.getByText(given.nb_relations)).toBeDefined() + expect(screen.getByText(given.nb_queries)).toBeDefined() + expect(screen.getByText(given.nb_types)).toBeDefined() + expect(screen.getByText(given.nb_rules)).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportStats/ReportStats.tsx b/cli/html-report/src/components/report/ReportStats/ReportStats.tsx new file mode 100644 index 000000000..e72b3c34b --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStats.tsx @@ -0,0 +1,19 @@ +import { useReport } from "@/hooks/useReport" +import { ReportStatsGrid } from "./ReportStatsGrid" + +export interface ReportStatsProps {} + +export const ReportStats = () => { + const { dbStats, violationStats } = useReport() + + return ( + + ) +} diff --git a/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.test.tsx b/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.test.tsx new file mode 100644 index 000000000..ab9e8cea5 --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from "@testing-library/react" +import { ReportStatsGrid, ReportStatsGridProps } from "./ReportStatsGrid" + +const props: ReportStatsGridProps = { + entities: 47, + relations: 44, + queries: 169, + types: 20, + rules: 27, + violations: { + high: 6, + medium: 88, + low: 0, + hint: 57, + }, +} + +describe("ReportStat", () => { + test("should render entities count", () => { + render() + expect(screen.getByText(props.entities)).toBeDefined() + }) + + test("should render relations count", () => { + render() + expect(screen.getByText(props.queries)).toBeDefined() + }) + + test("should render queries count", () => { + render() + expect(screen.getByText(props.queries)).toBeDefined() + }) + + test("should render types count", () => { + render() + expect(screen.getByText(props.types)).toBeDefined() + }) + + test("should render rules count", () => { + render() + expect(screen.getByText(props.rules)).toBeDefined() + }) + + test("should render total violations count", () => { + const given = Object.values(props.violations).reduce( + (sum, level) => sum + level, + 0 + ) + render() + expect(screen.getByText(given)).toBeDefined() + }) + + test("should render high violations count", () => { + render() + expect(screen.getByText(props.violations.high!)).toBeDefined() + }) + + test("should render medium violations count", () => { + render() + expect(screen.getByText(props.violations.medium!)).toBeDefined() + }) + + test("should render low violations count", () => { + render() + expect(screen.getByText(props.violations.low!)).toBeDefined() + }) + + test("should render hint violations count", () => { + render() + expect(screen.getByText(props.violations.hint!)).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.tsx b/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.tsx new file mode 100644 index 000000000..645806574 --- /dev/null +++ b/cli/html-report/src/components/report/ReportStats/ReportStatsGrid.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react" +import { ReportStatCell, ReportStatCellProps } from "./ReportStatCell" +import { ViolationStats } from "@/hooks/useReport" + +export interface ReportStatsGridProps { + entities: number + relations: number + queries: number + types: number + violations: ViolationStats + rules: number +} + +export const ReportStatsGrid = ({ + entities, + relations, + queries, + types, + violations, + rules, +}: ReportStatsGridProps) => { + const totalViolations = useMemo( + () => Object.values(violations).reduce((sum, level) => sum + level, 0), + [violations] + ) + + const databaseStats: ReportStatCellProps[] = [ + { label: "Entities", value: String(entities) }, + { label: "Relatons", value: String(relations) }, + { label: "Queries", value: String(queries) }, + { label: "Types", value: String(types) }, + { label: "Rules", value: String(rules) }, + ] + + const violationsStats: ReportStatCellProps[] = [ + { label: "Total violations", value: String(totalViolations) }, + { label: "High", value: String(violations.high ?? 0) }, + { label: "Medium", value: String(violations.medium ?? 0) }, + { label: "Low", value: String(violations.low ?? 0) }, + { label: "Hint", value: String(violations.hint ?? 0) }, + ] + + return ( +
+
+ {databaseStats.map((cellProps) => ( + + ))} +
+
+ {violationsStats.map((cellProps) => ( + + ))} +
+
+ ) +} diff --git a/cli/html-report/src/components/report/ViolationsList/ViolationList.test.tsx b/cli/html-report/src/components/report/ViolationsList/ViolationList.test.tsx new file mode 100644 index 000000000..1b447083a --- /dev/null +++ b/cli/html-report/src/components/report/ViolationsList/ViolationList.test.tsx @@ -0,0 +1,75 @@ +import { render, screen } from "@testing-library/react" +import * as ReportContext from "../../../context/ReportContext" +import { ViolationsList } from "./ViolationsList" +import { reportContextFactory } from "@/context/reportContextTestTool" + +describe("ViolationsList", () => { + test("Should render list of rules name loaded from context", () => { + const contextValues = reportContextFactory({ + rules: [ + { + name: "duplicated index", + totalViolations: 5, + level: "high", + conf: {}, + violations: [ + { + message: + "Index mfa_factors_user_id_idx on auth.mfa_factors(user_id) can be deleted, it's covered by: factor_id_created_at_idx(user_id, created_at).", + }, + ], + }, + { + name: "entity not clean", + totalViolations: 1, + level: "high", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + }, + ], + }, + ], + }) + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + render() + expect(screen.getByText("duplicated index")).toBeDefined() + expect(screen.getByText("entity not clean")).toBeDefined() + }) + + test("Should not render rule name if no violations", () => { + const contextValues = reportContextFactory({ + rules: [ + { + level: "high", + name: "duplicated index", + totalViolations: 5, + conf: {}, + violations: [ + { + message: + "Index mfa_factors_user_id_idx on auth.mfa_factors(user_id) can be deleted, it's covered by: factor_id_created_at_idx(user_id, created_at).", + }, + ], + }, + { + name: "entity not clean", + totalViolations: 0, + level: "high", + conf: {}, + violations: [], + }, + ], + }) + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + render() + expect(screen.getByText("duplicated index")).toBeDefined() + expect(screen.findByText("entity not clean")).toMatchObject({}) + }) +}) diff --git a/cli/html-report/src/components/report/ViolationsList/ViolationListItem.test.tsx b/cli/html-report/src/components/report/ViolationsList/ViolationListItem.test.tsx new file mode 100644 index 000000000..38705652e --- /dev/null +++ b/cli/html-report/src/components/report/ViolationsList/ViolationListItem.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from "@testing-library/react" +import { ViolationsListItem } from "./ViolationsListItem" +import { AnalyzeReportViolation } from "@azimutt/models" + +const violations: AnalyzeReportViolation[] = [ + { + message: + "Index mfa_factors_user_id_idx on auth.mfa_factors(user_id) can be deleted, it's covered by: factor_id_created_at_idx(user_id, created_at).", + entity: { schema: "auth", entity: "mfa_factors" }, + attribute: ["user_id"], + extra: { + index: { + name: "mfa_factors_user_id_idx", + attrs: [["user_id"]], + definition: "btree (user_id)", + }, + coveredBy: [ + { + name: "factor_id_created_at_idx", + attrs: [["user_id"], ["created_at"]], + definition: "btree (user_id, created_at)", + }, + ], + }, + }, + { + message: + "Index refresh_tokens_instance_id_idx on auth.refresh_tokens(instance_id) can be deleted, it's covered by: refresh_tokens_instance_id_user_id_idx(instance_id, user_id).", + entity: { schema: "auth", entity: "refresh_tokens" }, + attribute: ["instance_id"], + extra: { + index: { + name: "refresh_tokens_instance_id_idx", + attrs: [["instance_id"]], + definition: "btree (instance_id)", + }, + coveredBy: [ + { + name: "refresh_tokens_instance_id_user_id_idx", + attrs: [["instance_id"], ["user_id"]], + definition: "btree (instance_id, user_id)", + }, + ], + }, + }, + { + message: + "Index sessions_user_id_idx on auth.sessions(user_id) can be deleted, it's covered by: user_id_created_at_idx(user_id, created_at).", + entity: { schema: "auth", entity: "sessions" }, + attribute: ["user_id"], + extra: { + index: { + name: "sessions_user_id_idx", + attrs: [["user_id"]], + definition: "btree (user_id)", + }, + coveredBy: [ + { + name: "user_id_created_at_idx", + attrs: [["user_id"], ["created_at"]], + definition: "btree (user_id, created_at)", + }, + ], + }, + }, +] + +describe("ViolationsListItem", () => { + test("Should render the rule name", () => { + render( + + ) + expect(screen.getByText("duplicated index")).toBeDefined() + }) + + test("Should render the rule level", () => { + render( + + ) + expect(screen.getByText("high")).toBeDefined() + }) + + test("Should render the violations", () => { + render( + + ) + expect(screen.getByText(violations[0].message)).toBeDefined() + expect(screen.getByText(violations[1].message)).toBeDefined() + expect(screen.getByText(violations[2].message)).toBeDefined() + }) + + test("Should tells how many more violations", () => { + render( + + ) + expect( + screen.getByText(`${5 - violations.length} more violations`) + ).toBeDefined() + }) +}) diff --git a/cli/html-report/src/components/report/ViolationsList/ViolationsList.tsx b/cli/html-report/src/components/report/ViolationsList/ViolationsList.tsx new file mode 100644 index 000000000..bd5d8da4e --- /dev/null +++ b/cli/html-report/src/components/report/ViolationsList/ViolationsList.tsx @@ -0,0 +1,22 @@ +import { useReport } from "@/hooks/useReport" +import { ViolationsListItem } from "./ViolationsListItem" + +export interface ViolationsListProps {} + +export const ViolationsList = () => { + const { filteredRules } = useReport() + + return ( +
+ {filteredRules.map((rule) => ( + + ))} +
+ ) +} diff --git a/cli/html-report/src/components/report/ViolationsList/ViolationsListItem.tsx b/cli/html-report/src/components/report/ViolationsList/ViolationsListItem.tsx new file mode 100644 index 000000000..5053c6674 --- /dev/null +++ b/cli/html-report/src/components/report/ViolationsList/ViolationsListItem.tsx @@ -0,0 +1,50 @@ +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { pluralize } from "@/lib/utils" +import { AnalyzeReportViolation } from "@azimutt/models" + +export interface ViolationsListItemProps { + name: string + level: string + violations?: AnalyzeReportViolation[] + totalViolations?: number +} + +export const ViolationsListItem = ({ + name, + level, + violations, + totalViolations, +}: ViolationsListItemProps) => { + const moreViolations = + (totalViolations ?? violations?.length ?? 0) - (violations?.length ?? 0) + + return ( + + +
+

{name}

+ {level} +
+

{pluralize(totalViolations ?? 0, "violation")}

+
+ + {Boolean(violations?.length) && ( +
+ {violations?.map(({ message }) => ( + + {message} + + ))} + + {moreViolations > 0 && ( + + {pluralize(moreViolations ?? 0, "more violation")} + + )} +
+ )} +
+
+ ) +} diff --git a/cli/html-report/src/components/ui/badge.tsx b/cli/html-report/src/components/ui/badge.tsx new file mode 100644 index 000000000..f000e3ef5 --- /dev/null +++ b/cli/html-report/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/cli/html-report/src/components/ui/button.tsx b/cli/html-report/src/components/ui/button.tsx new file mode 100644 index 000000000..0ba427735 --- /dev/null +++ b/cli/html-report/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/cli/html-report/src/components/ui/card.tsx b/cli/html-report/src/components/ui/card.tsx new file mode 100644 index 000000000..afa13ecfa --- /dev/null +++ b/cli/html-report/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/cli/html-report/src/components/ui/command.tsx b/cli/html-report/src/components/ui/command.tsx new file mode 100644 index 000000000..d34152d8b --- /dev/null +++ b/cli/html-report/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/cli/html-report/src/components/ui/dialog.tsx b/cli/html-report/src/components/ui/dialog.tsx new file mode 100644 index 000000000..c23630eb8 --- /dev/null +++ b/cli/html-report/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/cli/html-report/src/components/ui/multi-select.tsx b/cli/html-report/src/components/ui/multi-select.tsx new file mode 100644 index 000000000..9039cc3fb --- /dev/null +++ b/cli/html-report/src/components/ui/multi-select.tsx @@ -0,0 +1,330 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { + CheckIcon, + XCircle, + ChevronDown, + XIcon, + WandSparkles, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" + +const multiSelectVariants = cva( + "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", + { + variants: { + variant: { + default: + "border-foreground/10 text-foreground bg-card hover:bg-card/80", + secondary: + "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + inverted: "inverted", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] + onValueChange: (value: string[]) => void + defaultValue: string[] + placeholder?: string + animation?: number + maxCount?: number + className?: string +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = "Select options", + animation = 0, + maxCount = 3, + className, + ...props + }, + ref + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue) + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false) + const [isAnimating, setIsAnimating] = React.useState(false) + + React.useEffect(() => { + if (JSON.stringify(selectedValues) !== JSON.stringify(defaultValue)) { + setSelectedValues(defaultValue) + } + }, [defaultValue, selectedValues]) + + const handleInputKeyDown = ( + event: React.KeyboardEvent + ) => { + if (event.key === "Enter") { + setIsPopoverOpen(true) + } else if (event.key === "Backspace" && !event.currentTarget.value) { + const newSelectedValues = [...selectedValues] + newSelectedValues.pop() + setSelectedValues(newSelectedValues) + onValueChange(newSelectedValues) + } + } + + const toggleOption = (value: string) => { + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value] + setSelectedValues(newSelectedValues) + onValueChange(newSelectedValues) + } + + const handleClear = () => { + setSelectedValues([]) + onValueChange([]) + } + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev) + } + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount) + setSelectedValues(newSelectedValues) + onValueChange(newSelectedValues) + } + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear() + } else { + const allValues = options.map((option) => option.value) + setSelectedValues(allValues) + onValueChange(allValues) + } + } + + return ( + + + + + setIsPopoverOpen(false)} + > + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option) => { + const isSelected = selectedValues.includes(option.value) + return ( + toggleOption(option.value)} + style={{ pointerEvents: "auto", opacity: 1 }} + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ) + })} +
+ + +
+ {selectedValues.length > 0 && ( + <> + + Clear + + + + )} + + setIsPopoverOpen(false)} + style={{ pointerEvents: "auto", opacity: 1 }} + className="flex-1 justify-center cursor-pointer" + > + Close + +
+
+
+
+
+ {animation > 0 && selectedValues.length > 0 && ( + setIsAnimating(!isAnimating)} + /> + )} +
+ ) + } +) + +MultiSelect.displayName = "MultiSelect" diff --git a/cli/html-report/src/components/ui/popover.tsx b/cli/html-report/src/components/ui/popover.tsx new file mode 100644 index 000000000..bbba7e0eb --- /dev/null +++ b/cli/html-report/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/cli/html-report/src/components/ui/select.tsx b/cli/html-report/src/components/ui/select.tsx new file mode 100644 index 000000000..fe56d4d3a --- /dev/null +++ b/cli/html-report/src/components/ui/select.tsx @@ -0,0 +1,158 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/cli/html-report/src/components/ui/separator.tsx b/cli/html-report/src/components/ui/separator.tsx new file mode 100644 index 000000000..6d7f12265 --- /dev/null +++ b/cli/html-report/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/cli/html-report/src/components/ui/table.tsx b/cli/html-report/src/components/ui/table.tsx new file mode 100644 index 000000000..7f3502f8b --- /dev/null +++ b/cli/html-report/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/cli/html-report/src/constants/env.constants.ts b/cli/html-report/src/constants/env.constants.ts new file mode 100644 index 000000000..b0d677248 --- /dev/null +++ b/cli/html-report/src/constants/env.constants.ts @@ -0,0 +1,3 @@ +const { MODE: ENVIRONMENT, PROD } = import.meta.env + +export { ENVIRONMENT, PROD } diff --git a/cli/html-report/src/constants/report.constants.ts b/cli/html-report/src/constants/report.constants.ts new file mode 100644 index 000000000..95d624303 --- /dev/null +++ b/cli/html-report/src/constants/report.constants.ts @@ -0,0 +1,1070 @@ +import { PROD } from "./env.constants" +import { AnalyzeReportHtmlResult } from "@azimutt/models" + +declare let __REPORT__: AnalyzeReportHtmlResult + +export const REPORT: AnalyzeReportHtmlResult = PROD + ? __REPORT__ + : ({ + rules: [ + { + name: "inconsistent attribute type", + level: "hint", + conf: {}, + totalViolations: 17, + violations: [ + { + message: + "Attribute id has several types: integer in storage.migrations(id), text in storage.buckets(id) and 1 other, bigint in auth.refresh_tokens(id) and 2 others, uuid in auth.audit_log_entries(id) and 32 others.", + entity: { schema: "storage", entity: "migrations" }, + attribute: ["id"], + extra: { + attributes: [ + { + schema: "auth", + entity: "audit_log_entries", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "flow_state", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "identities", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "instances", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "mfa_amr_claims", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "mfa_challenges", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "mfa_factors", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "one_time_tokens", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "refresh_tokens", + attribute: ["id"], + type: "bigint", + }, + { + schema: "auth", + entity: "saml_providers", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "saml_relay_states", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "sessions", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "sso_domains", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "sso_providers", + attribute: ["id"], + type: "uuid", + }, + { + schema: "auth", + entity: "users", + attribute: ["id"], + type: "uuid", + }, + { + schema: "pgsodium", + entity: "decrypted_key", + attribute: ["id"], + type: "uuid", + }, + { + schema: "pgsodium", + entity: "key", + attribute: ["id"], + type: "uuid", + }, + { + schema: "pgsodium", + entity: "valid_key", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "clever_cloud_resources", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "events", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "gallery", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "heroku_resources", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "organization_invitations", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "organizations", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "project_tokens", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "projects", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "user_auth_tokens", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "user_profiles", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "user_tokens", + attribute: ["id"], + type: "uuid", + }, + { + schema: "public", + entity: "users", + attribute: ["id"], + type: "uuid", + }, + { + schema: "realtime", + entity: "messages", + attribute: ["id"], + type: "bigint", + }, + { + schema: "realtime", + entity: "subscription", + attribute: ["id"], + type: "bigint", + }, + { + schema: "storage", + entity: "buckets", + attribute: ["id"], + type: "text", + }, + { + schema: "storage", + entity: "migrations", + attribute: ["id"], + type: "integer", + }, + { + schema: "storage", + entity: "objects", + attribute: ["id"], + type: "uuid", + }, + { + schema: "storage", + entity: "s3_multipart_uploads", + attribute: ["id"], + type: "text", + }, + { + schema: "storage", + entity: "s3_multipart_uploads_parts", + attribute: ["id"], + type: "uuid", + }, + { + schema: "vault", + entity: "decrypted_secrets", + attribute: ["id"], + type: "uuid", + }, + { + schema: "vault", + entity: "secrets", + attribute: ["id"], + type: "uuid", + }, + ], + }, + }, + { + message: + "Attribute created_at has several types: timestamp without time zone in auth.one_time_tokens(created_at) and 14 others, timestamp with time zone in auth.audit_log_entries(created_at) and 19 others.", + entity: { schema: "auth", entity: "one_time_tokens" }, + attribute: ["created_at"], + extra: { + attributes: [ + { + schema: "auth", + entity: "audit_log_entries", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "flow_state", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "identities", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "instances", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "mfa_amr_claims", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "mfa_challenges", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "mfa_factors", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "one_time_tokens", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "auth", + entity: "refresh_tokens", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "saml_providers", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "saml_relay_states", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "sessions", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "sso_domains", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "sso_providers", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "auth", + entity: "users", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "public", + entity: "clever_cloud_resources", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "events", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "gallery", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "heroku_resources", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "organization_invitations", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "organization_members", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "organizations", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "project_tokens", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "projects", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "user_auth_tokens", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "user_profiles", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "user_tokens", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "public", + entity: "users", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "realtime", + entity: "subscription", + attribute: ["created_at"], + type: "timestamp without time zone", + }, + { + schema: "storage", + entity: "buckets", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "storage", + entity: "objects", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "storage", + entity: "s3_multipart_uploads", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "storage", + entity: "s3_multipart_uploads_parts", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "vault", + entity: "decrypted_secrets", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + { + schema: "vault", + entity: "secrets", + attribute: ["created_at"], + type: "timestamp with time zone", + }, + ], + }, + }, + { + message: + "Attribute ip_address has several types: character varying(64) in auth.audit_log_entries(ip_address), inet in auth.mfa_challenges(ip_address).", + entity: { schema: "auth", entity: "audit_log_entries" }, + attribute: ["ip_address"], + extra: { + attributes: [ + { + schema: "auth", + entity: "audit_log_entries", + attribute: ["ip_address"], + type: "character varying(64)", + }, + { + schema: "auth", + entity: "mfa_challenges", + attribute: ["ip_address"], + type: "inet", + }, + ], + }, + }, + ], + }, + { + name: "expensive query", + level: "hint", + conf: {}, + totalViolations: 20, + violations: [ + { + message: + "Query 1374137181295181600 is one of the most expensive, cumulated 5986 ms exec time in 48 executions (SELECT name FROM pg_timezone_names)", + extra: { + queryId: "1374137181295181600", + query: "SELECT name FROM pg_timezone_names", + stats: { + rows: 58656, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 48, + minTime: 59.496679, + maxTime: 999.19821, + sumTime: 5985.892282, + meanTime: 124.706089208333, + sdTime: 216.974206280726, + }, + blocks: { sumRead: 0, sumWrite: 0, sumHit: 0, sumDirty: 0 }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [], + }, + }, + { + message: + "Query -156763288877666600 on pg_type is one of the most expensive, cumulated 1196 ms exec time in 48 executions ( WITH base_types AS ( WITH RECURSIVE recurse AS ( SELECT oid, typbasetype, COALESCE(NULLIF(typbasetype, $3), oid) AS base FROM pg_type UNION SELECT t.oid, b.typbasetype, COALESCE(NULLIF(b.typbasety...)", + entity: { entity: "pg_type" }, + extra: { + queryId: "-156763288877666600", + query: + "-- Recursively get the base types of domains\n WITH\n base_types AS (\n WITH RECURSIVE\n recurse AS (\n SELECT\n oid,\n typbasetype,\n COALESCE(NULLIF(typbasetype, $3), oid) AS base\n FROM pg_type\n UNION\n SELECT\n t.oid,\n b.typbasetype,\n COALESCE(NULLIF(b.typbasetype, $4), b.oid) AS base\n FROM recurse t\n JOIN pg_type b ON t.typbasetype = b.oid\n )\n SELECT\n oid,\n base\n FROM recurse\n WHERE typbasetype = $5\n ),\n arguments AS (\n SELECT\n oid,\n array_agg((\n COALESCE(name, $6), -- name\n type::regtype::text, -- type\n CASE type\n WHEN $7::regtype THEN $8\n WHEN $9::regtype THEN $10\n WHEN $11::regtype THEN $12\n WHEN $13::regtype THEN $14\n ELSE type::regtype::text\n END, -- convert types that ignore the lenth and accept any value till maximum size\n idx <= (pronargs - pronargdefaults), -- is_required\n COALESCE(mode = $15, $16) -- is_variadic\n ) ORDER BY idx) AS args,\n CASE COUNT(*) - COUNT(name) -- number of unnamed arguments\n WHEN $17 THEN $18\n WHEN $19 THEN (array_agg(type))[$20] IN ($21::regtype, $22::regtype, $23::regtype, $24::regtype, $25::regtype)\n ELSE $26\n END AS callable\n FROM pg_proc,\n unnest(proargnames, proargtypes, proargmodes)\n WITH ORDINALITY AS _ (name, type, mode, idx)\n WHERE type IS NOT NULL -- only input arguments\n GROUP BY oid\n )\n SELECT\n pn.nspname AS proc_schema,\n p.proname AS proc_name,\n d.description AS proc_description,\n COALESCE(a.args, $27) AS args,\n tn.nspname AS schema,\n COALESCE(comp.relname, t.typname) AS name,\n p.proretset AS rettype_is_setof,\n (t.typtype = $28\n -- if any TABLE, INOUT or OUT arguments present, treat as composite\n or COALESCE(proargmodes::text[] && $29, $30)\n ) AS rettype_is_composite,\n bt.oid <> bt.base as rettype_is_composite_alias,\n p.provolatile,\n p.provariadic > $31 as hasvariadic,\n lower((regexp_split_to_array((regexp_split_to_array(iso_config, $32))[$33], $34))[$35]) AS transaction_isolation_level,\n coalesce(func_settings.kvs, $36) as kvs\n FROM pg_proc p\n LEFT JOIN arguments a ON a.oid = p.oid\n JOIN pg_namespace pn ON pn.oid = p.pronamespace\n JOIN base_types bt ON bt.oid = p.prorettype\n JOIN pg_type t ON t.oid = bt.base\n JOIN pg_namespace tn ON tn.oid = t.typnamespace\n LEFT JOIN pg_class comp ON comp.oid = t.typrelid\n LEFT JOIN pg_description as d ON d.objoid = p.oid\n LEFT JOIN LATERAL unnest(proconfig) iso_config ON iso_config LIKE $37\n LEFT JOIN LATERAL (\n SELECT\n array_agg(row(\n substr(setting, $38, strpos(setting, $39) - $40),\n substr(setting, strpos(setting, $41) + $42)\n )) as kvs\n FROM unnest(proconfig) setting\n WHERE setting ~ ANY($2)\n ) func_settings ON $43\n WHERE t.oid <> $44::regtype AND COALESCE(a.callable, $45)\nAND prokind = $46 AND pn.nspname = ANY($1)", + stats: { + rows: 48, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 48, + minTime: 23.578641, + maxTime: 40.148259, + sumTime: 1196.34732, + meanTime: 24.9239025, + sdTime: 2.84173134574185, + }, + blocks: { + sumRead: 140, + sumWrite: 0, + sumHit: 99911, + sumDirty: 10, + }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [], + }, + }, + { + message: + "Query -8486453569861712000 on pg_attribute is one of the most expensive, cumulated 690 ms exec time in 55 executions (SELECT c.oid AS table_id , n.nspname AS tabl... FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_type t ON t.oid = a.atttypid LEFT JO...)", + entity: { entity: "pg_attribute" }, + extra: { + queryId: "-8486453569861712000", + query: + "SELECT c.oid AS table_id\n -- , u.rolname AS table_owner\n , n.nspname AS table_schema\n , c.relname AS table_name\n , c.relkind AS table_kind\n , a.attnum AS column_index\n , a.attname AS column_name\n , format_type(a.atttypid, a.atttypmod) AS column_type\n , t.typname AS column_type_name\n , t.typlen AS column_type_len\n , t.typcategory AS column_type_cat\n , NOT a.attnotnull AS column_nullable\n , pg_get_expr(ad.adbin, ad.adrelid) AS column_default\n , a.attgenerated = $1 AS column_generated\n , d.description AS column_comment\n , null_frac AS nulls\n , avg_width AS avg_len\n , n_distinct AS cardinality\n , most_common_vals AS common_vals\n , most_common_freqs AS common_freqs\n , histogram_bounds AS histogram\n FROM pg_attribute a\n JOIN pg_class c ON c.oid = a.attrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n -- JOIN pg_authid u ON u.oid = c.relowner\n JOIN pg_type t ON t.oid = a.atttypid\n LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum\n LEFT JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = a.attnum\n LEFT JOIN pg_stats s ON s.schemaname = n.nspname AND s.tablename = c.relname AND s.attname = a.attname\n WHERE c.relkind IN ($2, $3, $4)\n AND a.attnum > $5\n AND a.atttypid != $6\n AND n.nspname NOT IN ($7, $8)\n ORDER BY table_schema, table_name, column_index", + stats: { + rows: 26620, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 55, + minTime: 9.37997, + maxTime: 74.063108, + sumTime: 690.493505, + meanTime: 12.5544273636364, + sdTime: 9.6510871198741, + }, + blocks: { + sumRead: 41, + sumWrite: 0, + sumHit: 303988, + sumDirty: 10, + }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [ + { entity: "pg_attribute" }, + { entity: "pg_class" }, + { entity: "pg_namespace" }, + { entity: "pg_authid" }, + { entity: "pg_type" }, + { entity: "pg_attrdef" }, + { entity: "pg_description" }, + { entity: "pg_stats" }, + ], + }, + }, + ], + }, + { + name: "query with high variation", + level: "hint", + conf: {}, + totalViolations: 20, + violations: [ + { + message: + "Query 1374137181295181600 has high variation, with 217 ms standard deviation and exec time ranging from 59 ms to 999 ms (SELECT name FROM pg_timezone_names)", + extra: { + queryId: "1374137181295181600", + query: "SELECT name FROM pg_timezone_names", + stats: { + rows: 58656, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 48, + minTime: 59.496679, + maxTime: 999.19821, + sumTime: 5985.892282, + meanTime: 124.706089208333, + sdTime: 216.974206280726, + }, + blocks: { sumRead: 0, sumWrite: 0, sumHit: 0, sumDirty: 0 }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [], + }, + }, + { + message: + "Query 4509076507432270300 on pg_catalog.pg_constraint has high variation, with 27 ms standard deviation and exec time ranging from 12 ms to 142 ms (( with foreign_keys as ( select cl.relnamespace::regnamespace::text as schema_name, cl.relname as table_name, cl.oid as table_oid, ct.conname as fkey_name, ct.conkey as col_attnums from pg_catalog....)", + entity: { schema: "pg_catalog", entity: "pg_constraint" }, + extra: { + queryId: "4509076507432270300", + query: + "(\nwith foreign_keys as (\n select\n cl.relnamespace::regnamespace::text as schema_name,\n cl.relname as table_name,\n cl.oid as table_oid,\n ct.conname as fkey_name,\n ct.conkey as col_attnums\n from\n pg_catalog.pg_constraint ct\n join pg_catalog.pg_class cl -- fkey owning table\n on ct.conrelid = cl.oid\n left join pg_catalog.pg_depend d\n on d.objid = cl.oid\n and d.deptype = $1\n where\n ct.contype = $2 -- foreign key constraints\n and d.objid is null -- exclude tables that are dependencies of extensions\n and cl.relnamespace::regnamespace::text not in (\n $3, $4, $5, $6, $7, $8\n )\n),\nindex_ as (\n select\n pi.indrelid as table_oid,\n indexrelid::regclass as index_,\n string_to_array(indkey::text, $9)::smallint[] as col_attnums\n from\n pg_catalog.pg_index pi\n where\n indisvalid\n)\nselect\n $10 as name,\n $11 as title,\n $12 as level,\n $13 as facing,\n array[$14] as categories,\n $15 as description,\n format(\n $16,\n fk.schema_name,\n fk.table_name,\n fk.fkey_name\n ) as detail,\n $17 as remediation,\n jsonb_build_object(\n $18, fk.schema_name,\n $19, fk.table_name,\n $20, $21,\n $22, fk.fkey_name,\n $23, fk.col_attnums\n ) as metadata,\n format($24, fk.schema_name, fk.table_name, fk.fkey_name) as cache_key\nfrom\n foreign_keys fk\n left join index_ idx\n on fk.table_oid = idx.table_oid\n and fk.col_attnums = idx.col_attnums\n left join pg_catalog.pg_depend dep\n on idx.table_oid = dep.objid\n and dep.deptype = $25\nwhere\n idx.index_ is null\n and fk.schema_name not in (\n $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51\n )\n and dep.objid is null -- exclude tables owned by extensions\norder by\n fk.schema_name,\n fk.table_name,\n fk.fkey_name)\nunion all\n(\nselect\n $52 as name,\n $53 as title,\n $54 as level,\n $55 as facing,\n array[$56] as categories,\n $57 as description,\n format(\n $58,\n c.relname\n ) as detail,\n $59 as remediation,\n jsonb_build_object(\n $60, n.nspname,\n $61, c.relname,\n $62, $63,\n $64, array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege($65, c.oid, $66) then $67 when pg_catalog.has_table_privilege($68, c.oid, $69) then $70 end), $71)\n ) as metadata,\n format($72, n.nspname, c.relname) as cache_key\nfrom\n -- Identify the oid for auth.users\n pg_catalog.pg_class auth_users_pg_class\n join pg_catalog.pg_namespace auth_users_pg_namespace\n on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid\n and auth_users_pg_class.relname = $73\n and auth_users_pg_namespace.nspname = $74\n -- Depends on auth.users\n join pg_catalog.pg_depend d\n on d.refobjid = auth_users_pg_class.oid\n join pg_catalog.pg_rewrite r\n on r.oid = d.objid\n join pg_catalog.pg_class c\n on c.oid = r.ev_class\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n join pg_catalog.pg_class pg_class_auth_users\n on d.refobjid = pg_class_auth_users.oid\nwhere\n d.deptype = $75\n and (\n pg_catalog.has_table_privilege($76, c.oid, $77)\n or pg_catalog.has_table_privilege($78, c.oid, $79)\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting($80, $81), $82)))))\n -- Exclude self\n and c.relname <> $83\n -- There are 3 insecure configurations\n and\n (\n -- Materialized views don't support RLS so this is insecure by default\n (c.relkind in ($84)) -- m for materialized view\n or\n -- Standard View, accessible to anon or authenticated that is security_definer\n (\n c.relkind = $85 -- v for view\n -- Exclude security invoker views\n and not (\n lower(coalesce(c.reloptions::text,$86))::text[]\n && array[\n $87,\n $88,\n $89,\n $90\n ]\n )\n )\n or\n -- Standard View, security invoker, but no RLS enabled on auth.users\n (\n c.relkind in ($91) -- v for view\n -- is security invoker\n and (\n lower(coalesce(c.reloptions::text,$92))::text[]\n && array[\n $93,\n $94,\n $95,\n $96\n ]\n )\n and not pg_class_auth_users.relrowsecurity\n )\n )\ngroup by\n n.nspname,\n c.relname,\n c.oid)\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n polname as policy_name,\n polpermissive as is_permissive, -- if not, then restrictive\n (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n case polcmd\n when $97 then $98\n when $99 then $100\n when $101 then $102\n when $103 then $104\n when $105 then $106\n end as command,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n $107 as name,\n $108 as title,\n $109 as level,\n $110 as facing,\n array[$111] as categories,\n $112 as description,\n format(\n $113,\n schema_name,\n table_name,\n policy_name\n ) as detail,\n $114 as remediation,\n jsonb_build_object(\n $115, schema_name,\n $116, table_name,\n $117, $118\n ) as metadata,\n format($119, schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n is_rls_active\n and schema_name not in (\n $120, $121, $122, $123, $124, $125, $126, $127, $128, $129, $130, $131, $132, $133, $134, $135, $136, $137, $138, $139, $140, $141, $142, $143, $144, $145\n )\n and (\n -- Example: auth.uid()\n (\n qual like $146\n and lower(qual) not like $147\n )\n or (\n qual like $148\n and lower(qual) not like $149\n )\n or (\n qual like $150\n and lower(qual) not like $151\n )\n or (\n qual like $152\n and lower(qual) not like $153\n )\n or (\n with_check like $154\n and lower(with_check) not like $155\n )\n or (\n with_check like $156\n and lower(with_check) not like $157\n )\n or (\n with_check like $158\n and lower(with_check) not like $159\n )\n or (\n with_check like $160\n and lower(with_check) not like $161\n )\n ))\nunion all\n(\nselect\n $162 as name,\n $163 as title,\n $164 as level,\n $165 as facing,\n array[$166] as categories,\n $167 as description,\n format(\n $168,\n pgns.nspname,\n pgc.relname\n ) as detail,\n $169 as remediation,\n jsonb_build_object(\n $170, pgns.nspname,\n $171, pgc.relname,\n $172, $173\n ) as metadata,\n format(\n $174,\n pgns.nspname,\n pgc.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class pgc\n join pg_catalog.pg_namespace pgns\n on pgns.oid = pgc.relnamespace\n left join pg_catalog.pg_index pgi\n on pgi.indrelid = pgc.oid\n left join pg_catalog.pg_depend dep\n on pgc.oid = dep.objid\n and dep.deptype = $175\nwhere\n pgc.relkind = $176 -- regular tables\n and pgns.nspname not in (\n $177, $178, $179, $180, $181, $182, $183, $184, $185, $186, $187, $188, $189, $190, $191, $192, $193, $194, $195, $196, $197, $198, $199, $200, $201, $202\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n pgc.oid,\n pgns.nspname,\n pgc.relname\nhaving\n max(coalesce(pgi.indisprimary, $203)::int) = $204)\nunion all\n(\nselect\n $205 as name,\n $206 as title,\n $207 as level,\n $208 as facing,\n array[$209] as categories,\n $210 as description,\n format(\n $211,\n psui.indexrelname,\n psui.schemaname,\n psui.relname\n ) as detail,\n $212 as remediation,\n jsonb_build_object(\n $213, psui.schemaname,\n $214, psui.relname,\n $215, $216\n ) as metadata,\n format(\n $217,\n psui.schemaname,\n psui.relname,\n psui.indexrelname\n ) as cache_key\n\nfrom\n pg_catalog.pg_stat_user_indexes psui\n join pg_catalog.pg_index pi\n on psui.indexrelid = pi.indexrelid\n left join pg_catalog.pg_depend dep\n on psui.relid = dep.objid\n and dep.deptype = $218\nwhere\n psui.idx_scan = $219\n and not pi.indisunique\n and not pi.indisprimary\n and dep.objid is null -- exclude tables owned by extensions\n and psui.schemaname not in (\n $220, $221, $222, $223, $224, $225, $226, $227, $228, $229, $230, $231, $232, $233, $234, $235, $236, $237, $238, $239, $240, $241, $242, $243, $244, $245\n ))\nunion all\n(\nselect\n $246 as name,\n $247 as title,\n $248 as level,\n $249 as facing,\n array[$250] as categories,\n $251 as description,\n format(\n $252,\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd,\n array_agg(p.polname order by p.polname)\n ) as detail,\n $253 as remediation,\n jsonb_build_object(\n $254, n.nspname,\n $255, c.relname,\n $256, $257\n ) as metadata,\n format(\n $258,\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_roles r\n on p.polroles @> array[r.oid]\n or p.polroles = array[$259::oid]\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $260,\n lateral (\n select x.cmd\n from unnest((\n select\n case p.polcmd\n when $261 then array[$262]\n when $263 then array[$264]\n when $265 then array[$266]\n when $267 then array[$268]\n when $269 then array[$270, $271, $272, $273]\n else array[$274]\n end as actions\n )) x(cmd)\n ) act(cmd)\nwhere\n c.relkind = $275 -- regular tables\n and p.polpermissive -- policy is permissive\n and n.nspname not in (\n $276, $277, $278, $279, $280, $281, $282, $283, $284, $285, $286, $287, $288, $289, $290, $291, $292, $293, $294, $295, $296, $297, $298, $299, $300, $301\n )\n and r.rolname not like $302\n and r.rolname not like $303\n and not r.rolbypassrls\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\nhaving\n count($304) > $305)\nunion all\n(\nselect\n $306 as name,\n $307 as title,\n $308 as level,\n $309 as facing,\n array[$310] as categories,\n $311 as description,\n format(\n $312,\n n.nspname,\n c.relname,\n array_agg(p.polname order by p.polname)\n ) as detail,\n $313 as remediation,\n jsonb_build_object(\n $314, n.nspname,\n $315, c.relname,\n $316, $317\n ) as metadata,\n format(\n $318,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $319\nwhere\n c.relkind = $320 -- regular tables\n and n.nspname not in (\n $321, $322, $323, $324, $325, $326, $327, $328, $329, $330, $331, $332, $333, $334, $335, $336, $337, $338, $339, $340, $341, $342, $343, $344, $345, $346\n )\n -- RLS is disabled\n and not c.relrowsecurity\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n $347 as name,\n $348 as title,\n $349 as level,\n $350 as facing,\n array[$351] as categories,\n $352 as description,\n format(\n $353,\n n.nspname,\n c.relname\n ) as detail,\n $354 as remediation,\n jsonb_build_object(\n $355, n.nspname,\n $356, c.relname,\n $357, $358\n ) as metadata,\n format(\n $359,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n left join pg_catalog.pg_policy p\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $360\nwhere\n c.relkind = $361 -- regular tables\n and n.nspname not in (\n $362, $363, $364, $365, $366, $367, $368, $369, $370, $371, $372, $373, $374, $375, $376, $377, $378, $379, $380, $381, $382, $383, $384, $385, $386, $387\n )\n -- RLS is enabled\n and c.relrowsecurity\n and p.polname is null\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\nunion all\n(\nselect\n $388 as name,\n $389 as title,\n $390 as level,\n $391 as facing,\n array[$392] as categories,\n $393 as description,\n format(\n $394,\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as detail,\n $395 as remediation,\n jsonb_build_object(\n $396, n.nspname,\n $397, c.relname,\n $398, case\n when c.relkind = $399 then $400\n when c.relkind = $401 then $402\n else $403\n end,\n $404, array_agg(pi.indexname order by pi.indexname)\n ) as metadata,\n format(\n $405,\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as cache_key\nfrom\n pg_catalog.pg_indexes pi\n join pg_catalog.pg_namespace n\n on n.nspname = pi.schemaname\n join pg_catalog.pg_class c\n on pi.tablename = c.relname\n and n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $406\nwhere\n c.relkind in ($407, $408) -- tables and materialized views\n and n.nspname not in (\n $409, $410, $411, $412, $413, $414, $415, $416, $417, $418, $419, $420, $421, $422, $423, $424, $425, $426, $427, $428, $429, $430, $431, $432, $433, $434\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relkind,\n c.relname,\n replace(pi.indexdef, pi.indexname, $435)\nhaving\n count(*) > $436)\nunion all\n(\nselect\n $437 as name,\n $438 as title,\n $439 as level,\n $440 as facing,\n array[$441] as categories,\n $442 as description,\n format(\n $443,\n n.nspname,\n c.relname\n ) as detail,\n $444 as remediation,\n jsonb_build_object(\n $445, n.nspname,\n $446, c.relname,\n $447, $448\n ) as metadata,\n format(\n $449,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $450\nwhere\n c.relkind = $451\n and (\n pg_catalog.has_table_privilege($452, c.oid, $453)\n or pg_catalog.has_table_privilege($454, c.oid, $455)\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting($456, $457), $458)))))\n and n.nspname not in (\n $459, $460, $461, $462, $463, $464, $465, $466, $467, $468, $469, $470, $471, $472, $473, $474, $475, $476, $477, $478, $479, $480, $481, $482, $483, $484\n )\n and dep.objid is null -- exclude views owned by extensions\n and not (\n lower(coalesce(c.reloptions::text,$485))::text[]\n && array[\n $486,\n $487,\n $488,\n $489\n ]\n ))\nunion all\n(\nselect\n $490 as name,\n $491 as title,\n $492 as level,\n $493 as facing,\n array[$494] as categories,\n $495 as description,\n format(\n $496,\n n.nspname,\n p.proname\n ) as detail,\n $497 as remediation,\n jsonb_build_object(\n $498, n.nspname,\n $499, p.proname,\n $500, $501\n ) as metadata,\n format(\n $502,\n n.nspname,\n p.proname,\n md5(p.prosrc) -- required when function is polymorphic\n ) as cache_key\nfrom\n pg_catalog.pg_proc p\n join pg_catalog.pg_namespace n\n on p.pronamespace = n.oid\n left join pg_catalog.pg_depend dep\n on p.oid = dep.objid\n and dep.deptype = $503\nwhere\n n.nspname not in (\n $504, $505, $506, $507, $508, $509, $510, $511, $512, $513, $514, $515, $516, $517, $518, $519, $520, $521, $522, $523, $524, $525, $526, $527, $528, $529\n )\n and dep.objid is null -- exclude functions owned by extensions\n -- Search path not set to ''\n and not coalesce(p.proconfig, $530) && array[$531])\nunion all\n(\nselect\n $532 as name,\n $533 as title,\n $534 as level,\n $535 as facing,\n array[$536] as categories,\n $537 as description,\n format(\n $538,\n n.nspname,\n c.relname\n ) as detail,\n $539 as remediation,\n jsonb_build_object(\n $540, n.nspname,\n $541, c.relname,\n $542, $543\n ) as metadata,\n format(\n $544,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind = $545 -- regular tables\n -- RLS is disabled\n and not c.relrowsecurity\n and (\n pg_catalog.has_table_privilege($546, c.oid, $547)\n or pg_catalog.has_table_privilege($548, c.oid, $549)\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting($550, $551), $552)))))\n and n.nspname not in (\n $553, $554, $555, $556, $557, $558, $559, $560, $561, $562, $563, $564, $565, $566, $567, $568, $569, $570, $571, $572, $573, $574, $575, $576, $577, $578\n ))\nunion all\n(\nselect\n $579 as name,\n $580 as title,\n $581 as level,\n $582 as facing,\n array[$583] as categories,\n $584 as description,\n format(\n $585,\n pe.extname\n ) as detail,\n $586 as remediation,\n jsonb_build_object(\n $587, pe.extnamespace::regnamespace,\n $588, pe.extname,\n $589, $590\n ) as metadata,\n format(\n $591,\n pe.extname\n ) as cache_key\nfrom\n pg_catalog.pg_extension pe\nwhere\n -- plpgsql is installed by default in public and outside user control\n -- confirmed safe\n pe.extname not in ($592)\n -- Scoping this to public is not optimal. Ideally we would use the postgres\n -- search path. That currently isn't available via SQL. In other lints\n -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that\n -- is not appropriate here as it would evaluate true for the extensions schema\n and pe.extnamespace::regnamespace::text = $593)\nunion all\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n polname as policy_name,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n $594 as name,\n $595 as title,\n $596 as level,\n $597 as facing,\n array[$598] as categories,\n $599 as description,\n format(\n $600,\n schema_name,\n table_name,\n policy_name\n ) as detail,\n $601 as remediation,\n jsonb_build_object(\n $602, schema_name,\n $603, table_name,\n $604, $605\n ) as metadata,\n format($606, schema_name, table_name, policy_name) as cache_key\nfrom\n policies\nwhere\n schema_name not in (\n $607, $608, $609, $610, $611, $612, $613, $614, $615, $616, $617, $618, $619, $620, $621, $622, $623, $624, $625, $626, $627, $628, $629, $630, $631, $632\n )\n and (\n -- Example: auth.jwt() -> 'user_metadata'\n -- False positives are possible, but it isn't practical to string match\n -- If false positive rate is too high, this expression can iterate\n qual like $633\n or qual like $634\n or with_check like $635\n or with_check like $636\n ))\nunion all\n(\nselect\n $637 as name,\n $638 as title,\n $639 as level,\n $640 as facing,\n array[$641] as categories,\n $642 as description,\n format(\n $643,\n n.nspname,\n c.relname\n ) as detail,\n $644 as remediation,\n jsonb_build_object(\n $645, n.nspname,\n $646, c.relname,\n $647, $648\n ) as metadata,\n format(\n $649,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $650\nwhere\n c.relkind = $651\n and (\n pg_catalog.has_table_privilege($652, c.oid, $653)\n or pg_catalog.has_table_privilege($654, c.oid, $655)\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting($656, $657), $658)))))\n and n.nspname not in (\n $659, $660, $661, $662, $663, $664, $665, $666, $667, $668, $669, $670, $671, $672, $673, $674, $675, $676, $677, $678, $679, $680, $681, $682, $683, $684\n )\n and dep.objid is null)\nunion all\n(\nselect\n $685 as name,\n $686 as title,\n $687 as level,\n $688 as facing,\n array[$689] as categories,\n $690 as description,\n format(\n $691,\n n.nspname,\n c.relname\n ) as detail,\n $692 as remediation,\n jsonb_build_object(\n $693, n.nspname,\n $694, c.relname,\n $695, $696\n ) as metadata,\n format(\n $697,\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = $698\nwhere\n c.relkind = $699\n and (\n pg_catalog.has_table_privilege($700, c.oid, $701)\n or pg_catalog.has_table_privilege($702, c.oid, $703)\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting($704, $705), $706)))))\n and n.nspname not in (\n $707, $708, $709, $710, $711, $712, $713, $714, $715, $716, $717, $718, $719, $720, $721, $722, $723, $724, $725, $726, $727, $728, $729, $730, $731, $732\n )\n and dep.objid is null)", + stats: { + rows: 843, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 21, + minTime: 12.349852, + maxTime: 141.544165, + sumTime: 477.24553, + meanTime: 22.7259776190476, + sdTime: 26.9726899551051, + }, + blocks: { + sumRead: 17, + sumWrite: 0, + sumHit: 110129, + sumDirty: 1, + }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [], + }, + }, + { + message: + "Query -4726471486296252000 on pg_attribute has high variation, with 21 ms standard deviation and exec time ranging from 5 ms to 78 ms (SELECT t.oid, t.typname, t.typsend, t.typrec... FROM pg_catalog.pg_type s WHERE s.typrelid != $7 AND s.oid = t.typelem)))", + entity: { entity: "pg_attribute" }, + extra: { + queryId: "-4726471486296252000", + query: + "SELECT t.oid, t.typname, t.typsend, t.typreceive, t.typoutput, t.typinput,\n coalesce(d.typelem, t.typelem), coalesce(r.rngsubtype, $1), ARRAY (\n SELECT a.atttypid\n FROM pg_attribute AS a\n WHERE a.attrelid = t.typrelid AND a.attnum > $2 AND NOT a.attisdropped\n ORDER BY a.attnum\n)\nFROM pg_type AS t\nLEFT JOIN pg_type AS d ON t.typbasetype = d.oid\nLEFT JOIN pg_range AS r ON r.rngtypid = t.oid OR r.rngmultitypid = t.oid OR (t.typbasetype <> $3 AND r.rngtypid = t.typbasetype)\nWHERE (t.typrelid = $4)\nAND (t.typelem = $5 OR NOT EXISTS (SELECT $6 FROM pg_catalog.pg_type s WHERE s.typrelid != $7 AND s.oid = t.typelem))", + stats: { + rows: 2568, + plan: { + count: 0, + minTime: 0, + maxTime: 0, + sumTime: 0, + meanTime: 0, + sdTime: 0, + }, + exec: { + count: 12, + minTime: 4.743659, + maxTime: 77.626603, + sumTime: 209.676455, + meanTime: 17.4730379166667, + sdTime: 21.1893507662803, + }, + blocks: { + sumRead: 27, + sumWrite: 0, + sumHit: 31705, + sumDirty: 0, + }, + blocksTmp: { + sumRead: 0, + sumWrite: 0, + sumHit: 0, + sumDirty: 0, + }, + blocksQuery: { sumRead: 0, sumWrite: 0 }, + }, + entities: [ + { entity: "pg_attribute" }, + { entity: "pg_type" }, + { entity: "pg_type", schema: "pg_catalog" }, + { entity: "pg_type" }, + { entity: "pg_range" }, + ], + }, + }, + ], + }, + { + name: "entity too large", + level: "medium", + conf: { max: 30 }, + totalViolations: 2, + violations: [ + { + message: "Entity auth.users has too many attributes (35).", + entity: { schema: "auth", entity: "users" }, + extra: { attributes: 35 }, + }, + { + message: + "Entity extensions.pg_stat_statements has too many attributes (43).", + entity: { schema: "extensions", entity: "pg_stat_statements" }, + extra: { attributes: 43 }, + }, + ], + }, + { + name: "entity with too heavy indexes", + level: "medium", + conf: { ratio: 1 }, + totalViolations: 15, + violations: [ + { + message: + "Entity auth.users has too heavy indexes (10x data size, 11 indexes).", + entity: { schema: "auth", entity: "users" }, + extra: { ratio: 10 }, + }, + { + message: + "Entity public.gallery has too heavy indexes (6x data size, 3 indexes).", + entity: { schema: "public", entity: "gallery" }, + extra: { ratio: 6 }, + }, + { + message: + "Entity public.organizations has too heavy indexes (6x data size, 3 indexes).", + entity: { schema: "public", entity: "organizations" }, + extra: { ratio: 6 }, + }, + ], + }, + { + name: "business primary key forbidden", + level: "medium", + conf: {}, + totalViolations: 3, + violations: [ + { + message: + "Entity auth.schema_migrations should have a technical primary key, current one is: (version).", + entity: { schema: "auth", entity: "schema_migrations" }, + attribute: ["version"], + extra: { + primaryKey: { + name: "schema_migrations_pkey", + attrs: [["version"]], + }, + }, + }, + { + message: + "Entity public.schema_migrations should have a technical primary key, current one is: (version).", + entity: { schema: "public", entity: "schema_migrations" }, + attribute: ["version"], + extra: { + primaryKey: { + name: "schema_migrations_pkey", + attrs: [["version"]], + }, + }, + }, + { + message: + "Entity realtime.schema_migrations should have a technical primary key, current one is: (version).", + entity: { schema: "realtime", entity: "schema_migrations" }, + attribute: ["version"], + extra: { + primaryKey: { + name: "schema_migrations_pkey", + attrs: [["version"]], + }, + }, + }, + ], + }, + { + name: "index on relation", + level: "medium", + conf: {}, + totalViolations: 26, + violations: [ + { + message: + "Create an index on auth.mfa_challenges(factor_id) to improve auth.mfa_challenges(factor_id)->auth.mfa_factors(id) relation.", + entity: { schema: "auth", entity: "mfa_challenges" }, + attribute: ["factor_id"], + extra: { + indexAttrs: [["factor_id"]], + relation: { + name: "mfa_challenges_auth_factor_id_fkey", + src: { schema: "auth", entity: "mfa_challenges" }, + ref: { schema: "auth", entity: "mfa_factors" }, + attrs: [{ src: ["factor_id"], ref: ["id"] }], + }, + }, + }, + { + message: + "Create an index on auth.saml_relay_states(flow_state_id) to improve auth.saml_relay_states(flow_state_id)->auth.flow_state(id) relation.", + entity: { schema: "auth", entity: "saml_relay_states" }, + attribute: ["flow_state_id"], + extra: { + indexAttrs: [["flow_state_id"]], + relation: { + name: "saml_relay_states_flow_state_id_fkey", + src: { schema: "auth", entity: "saml_relay_states" }, + ref: { schema: "auth", entity: "flow_state" }, + attrs: [{ src: ["flow_state_id"], ref: ["id"] }], + }, + }, + }, + { + message: + "Create an index on pgsodium.key(parent_key) to improve pgsodium.key(parent_key)->pgsodium.key(id) relation.", + entity: { schema: "pgsodium", entity: "key" }, + attribute: ["parent_key"], + extra: { + indexAttrs: [["parent_key"]], + relation: { + name: "key_parent_key_fkey", + src: { schema: "pgsodium", entity: "key" }, + ref: { schema: "pgsodium", entity: "key" }, + attrs: [{ src: ["parent_key"], ref: ["id"] }], + }, + }, + }, + ], + }, + { + name: "missing relation", + level: "medium", + conf: {}, + totalViolations: 42, + violations: [ + { + message: + "Create a relation from auth.audit_log_entries(instance_id) to auth.instances(id).", + entity: { schema: "auth", entity: "audit_log_entries" }, + attribute: ["instance_id"], + extra: { + relation: { + src: { schema: "auth", entity: "audit_log_entries" }, + ref: { schema: "auth", entity: "instances" }, + attrs: [{ src: ["instance_id"], ref: ["id"] }], + origin: "infer-name", + }, + }, + }, + { + message: + "Create a relation from auth.flow_state(user_id) to auth.users(id).", + entity: { schema: "auth", entity: "flow_state" }, + attribute: ["user_id"], + extra: { + relation: { + src: { schema: "auth", entity: "flow_state" }, + ref: { schema: "auth", entity: "users" }, + attrs: [{ src: ["user_id"], ref: ["id"] }], + origin: "infer-name", + }, + }, + }, + { + message: + "Create a relation from auth.flow_state(user_id) to public.users(id).", + entity: { schema: "auth", entity: "flow_state" }, + attribute: ["user_id"], + extra: { + relation: { + src: { schema: "auth", entity: "flow_state" }, + ref: { schema: "public", entity: "users" }, + attrs: [{ src: ["user_id"], ref: ["id"] }], + origin: "infer-name", + }, + }, + }, + ], + }, + { + name: "duplicated index", + level: "high", + conf: {}, + totalViolations: 5, + violations: [ + { + message: + "Index mfa_factors_user_id_idx on auth.mfa_factors(user_id) can be deleted, it's covered by: factor_id_created_at_idx(user_id, created_at).", + entity: { schema: "auth", entity: "mfa_factors" }, + attribute: ["user_id"], + extra: { + index: { + name: "mfa_factors_user_id_idx", + attrs: [["user_id"]], + definition: "btree (user_id)", + }, + coveredBy: [ + { + name: "factor_id_created_at_idx", + attrs: [["user_id"], ["created_at"]], + definition: "btree (user_id, created_at)", + }, + ], + }, + }, + { + message: + "Index refresh_tokens_instance_id_idx on auth.refresh_tokens(instance_id) can be deleted, it's covered by: refresh_tokens_instance_id_user_id_idx(instance_id, user_id).", + entity: { schema: "auth", entity: "refresh_tokens" }, + attribute: ["instance_id"], + extra: { + index: { + name: "refresh_tokens_instance_id_idx", + attrs: [["instance_id"]], + definition: "btree (instance_id)", + }, + coveredBy: [ + { + name: "refresh_tokens_instance_id_user_id_idx", + attrs: [["instance_id"], ["user_id"]], + definition: "btree (instance_id, user_id)", + }, + ], + }, + }, + { + message: + "Index sessions_user_id_idx on auth.sessions(user_id) can be deleted, it's covered by: user_id_created_at_idx(user_id, created_at).", + entity: { schema: "auth", entity: "sessions" }, + attribute: ["user_id"], + extra: { + index: { + name: "sessions_user_id_idx", + attrs: [["user_id"]], + definition: "btree (user_id)", + }, + coveredBy: [ + { + name: "user_id_created_at_idx", + attrs: [["user_id"], ["created_at"]], + definition: "btree (user_id, created_at)", + }, + ], + }, + }, + ], + }, + { + name: "entity not clean", + level: "high", + conf: { + maxDeadRows: 30000, + maxVacuumLag: 30000, + maxAnalyzeLag: 30000, + maxVacuumDelayMs: 86400000, + maxAnalyzeDelayMs: 86400000, + }, + totalViolations: 1, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + }, + ], + stats: { + nb_entities: 47, + nb_relations: 44, + nb_queries: 169, + nb_types: 20, + nb_rules: 27, + }, + } as AnalyzeReportHtmlResult) diff --git a/cli/html-report/src/context/ReportContext.tsx b/cli/html-report/src/context/ReportContext.tsx new file mode 100644 index 000000000..ede87a412 --- /dev/null +++ b/cli/html-report/src/context/ReportContext.tsx @@ -0,0 +1,19 @@ +import { REPORT } from "@/constants/report.constants" +import { AnalyzeReportHtmlResult, RuleLevel } from "@azimutt/models" +import { createContext, useContext } from "react" + +export interface ReportContextFilters { + levels?: RuleLevel[] + categories?: string[] + rules?: string[] + tables?: string[] +} + +export interface ReportContext { + report: AnalyzeReportHtmlResult + filters?: ReportContextFilters +} + +export const ReportContext = createContext({ report: REPORT }) + +export const useReportContext = () => useContext(ReportContext) diff --git a/cli/html-report/src/context/reportContextTestTool.ts b/cli/html-report/src/context/reportContextTestTool.ts new file mode 100644 index 000000000..736d105b4 --- /dev/null +++ b/cli/html-report/src/context/reportContextTestTool.ts @@ -0,0 +1,22 @@ +import { AnalyzeReportHtmlResult } from "@azimutt/models" +import type { ReportContext, ReportContextFilters } from "./ReportContext" + +export const reportContextFactory = ( + report?: Partial, + filters?: Partial +): ReportContext => ({ + report: { + rules: [], + ...(report ?? {}), + + stats: { + nb_entities: 0, + nb_relations: 0, + nb_queries: 0, + nb_types: 0, + nb_rules: 0, + ...(report?.stats ?? {}), + }, + }, + filters, +}) diff --git a/cli/html-report/src/hooks/useReport.test.tsx b/cli/html-report/src/hooks/useReport.test.tsx new file mode 100644 index 000000000..8c60c9880 --- /dev/null +++ b/cli/html-report/src/hooks/useReport.test.tsx @@ -0,0 +1,213 @@ +import { renderHook } from "@testing-library/react" +import { useReport } from "./useReport" +import * as ReportContext from "@/context/ReportContext" +import { reportContextFactory } from "@/context/reportContextTestTool" + +describe("useReport", () => { + test("should filter by level", () => { + const contextValues = reportContextFactory( + { + rules: [ + { + level: "high", + name: "Rule1", + conf: {}, + violations: [], + totalViolations: 1, + }, + { + level: "medium", + name: "Rule2", + conf: {}, + violations: [], + totalViolations: 1, + }, + ], + }, + { + levels: ["high"], + } + ) + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + + const { result } = renderHook(() => useReport()) + expect(result.current.filteredRules).toHaveLength(1) + expect(result.current.filteredRules[0].level).toBe("high") + }) + + test("should filter by rule", () => { + const contextValues = reportContextFactory( + { + rules: [ + { + name: "duplicated index", + level: "high", + conf: {}, + violations: [], + totalViolations: 12, + }, + { + name: "entity not clean", + level: "medium", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 1, + }, + ], + }, + { rules: ["duplicated index"] } + ) + + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + + const { result } = renderHook(() => useReport()) + expect(result.current.filteredRules).toHaveLength(1) + expect(result.current.filteredRules[0].name).toBe("duplicated index") + }) + + test("Should filter by tables", () => { + const contextValues = reportContextFactory( + { + rules: [ + { + name: "duplicated index", + level: "high", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "private", entity: "logs" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 12, + }, + { + name: "entity not clean", + level: "medium", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 1, + }, + ], + }, + { tables: ["public.events"] } + ) + + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + + const { result } = renderHook(() => useReport()) + expect(result.current.filteredRules).toHaveLength(1) + expect(result.current.filteredRules[0].name).toBe("entity not clean") + }) + + test("Should extract tables", () => { + const contextValues = reportContextFactory({ + rules: [ + { + name: "entity not clean", + level: "medium", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 1, + }, + ], + }) + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + + const { result } = renderHook(() => useReport()) + expect(result.current.tables).toHaveLength(1) + expect(result.current.tables).toContain("public.events") + }) + + test("Should extract distinct tables", () => { + const contextValues = reportContextFactory({ + rules: [ + { + level: "high", + name: "duplicated index", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 12, + }, + { + level: "medium", + name: "entity not clean", + conf: {}, + violations: [ + { + message: + "Entity public.events has old analyze (2024-06-17T10:18:35.009Z).", + entity: { schema: "public", entity: "events" }, + extra: { + reason: "old analyze", + value: "2024-06-17T10:18:35.009Z", + }, + }, + ], + totalViolations: 1, + }, + ], + }) + + jest + .spyOn(ReportContext, "useReportContext") + .mockImplementation(() => contextValues) + + const { result } = renderHook(() => useReport()) + expect(result.current.tables).toHaveLength(1) + expect(result.current.tables).toContain("public.events") + }) +}) diff --git a/cli/html-report/src/hooks/useReport.tsx b/cli/html-report/src/hooks/useReport.tsx new file mode 100644 index 000000000..34babdf12 --- /dev/null +++ b/cli/html-report/src/hooks/useReport.tsx @@ -0,0 +1,98 @@ +import { useReportContext } from "@/context/ReportContext" +import { AnalyzeReportRule, AnalyzeReportViolation } from "@azimutt/models" +import { useMemo } from "react" + +export interface FilterableAnalyzeReportRule extends AnalyzeReportRule { + entities: string[] +} + +export interface ViolationStats { + high?: number + medium?: number + low?: number + hint?: number +} + +export function useReport() { + const { report, filters } = useReportContext() + + const filterableRules = useMemo(() => { + const { rules } = report + return rules.map((rule) => ({ + ...rule, + entities: rule.violations.reduce((acc, violation) => { + if (!violation?.entity) return acc + const entity = [violation.entity.schema, violation.entity.entity].join( + "." + ) + if (!acc.includes(entity)) { + acc.push(entity) + } + return acc + }, []), + })) + }, [report]) + + const filteredRules = useMemo(() => { + return filterableRules.filter( + (rule) => + rule.totalViolations > 0 && + (!filters?.levels?.length || filters.levels.includes(rule.level)) && + (!filters?.rules?.length || filters.rules.includes(rule.name)) && + (!filters?.tables?.length || + filters.tables.some((table) => rule.entities.includes(table))) + ) + }, [filters, filterableRules]) + + const rules = useMemo(() => { + const { rules } = report + const ruleNames = rules.reduce((acc, rule) => { + if (!acc.includes(rule.name)) { + acc.push(rule.name) + } + return acc + }, []) + return ruleNames.sort() + }, [report]) + + const tables = useMemo(() => { + const { rules } = report + const violations = rules.reduce((acc, rule) => { + acc.push(...rule.violations) + return acc + }, []) + const tables = violations.reduce((acc, violation) => { + if (!violation?.entity) return acc + const fullName = [violation.entity.schema, violation.entity.entity].join( + "." + ) + if (!acc.includes(fullName)) { + acc.push(fullName) + } + return acc + }, []) + return tables + }, [report]) + + const violationStats = useMemo(() => { + const { rules } = report + + return rules.reduce((acc, rule) => { + if (rule.level === "off") return acc + if (!acc[rule.level]) { + acc[rule.level] = 0 + } + acc[rule.level]! += 1 + return acc + }, {}) + }, [report]) + + return { + filters, + filteredRules, + rules, + tables, + violationStats, + dbStats: report.stats, + } +} diff --git a/cli/html-report/src/lib/utils.ts b/cli/html-report/src/lib/utils.ts new file mode 100644 index 000000000..def7ac7e6 --- /dev/null +++ b/cli/html-report/src/lib/utils.ts @@ -0,0 +1,38 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function plural(word: string): string { + if ( + word.endsWith("y") && + !( + word.endsWith("ay") || + word.endsWith("ey") || + word.endsWith("oy") || + word.endsWith("uy") + ) + ) { + return word.slice(0, -1) + "ies" + } else if ( + word.endsWith("s") || + word.endsWith("x") || + word.endsWith("z") || + word.endsWith("sh") || + word.endsWith("ch") + ) { + return word + "es" + } else { + return word + "s" + } +} + +export function pluralize(count: number, word: string): string { + if (count === 1) { + return `1 ${word}` + } else { + return `${count} ${plural(word)}` + } +} diff --git a/cli/html-report/src/main.tsx b/cli/html-report/src/main.tsx new file mode 100644 index 000000000..5e5edc576 --- /dev/null +++ b/cli/html-report/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import '../app/globals.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/cli/html-report/src/test/__ mocks __/fileMock.js b/cli/html-report/src/test/__ mocks __/fileMock.js new file mode 100644 index 000000000..3401b1c59 --- /dev/null +++ b/cli/html-report/src/test/__ mocks __/fileMock.js @@ -0,0 +1,4 @@ +module.exports = { + __esModule: true, + default: "test-file-stub", +} diff --git a/cli/html-report/src/vite-env.d.ts b/cli/html-report/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/cli/html-report/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/cli/html-report/tailwind.config.js b/cli/html-report/tailwind.config.js new file mode 100644 index 000000000..7cb7e37ab --- /dev/null +++ b/cli/html-report/tailwind.config.js @@ -0,0 +1,77 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} \ No newline at end of file diff --git a/cli/html-report/tsconfig.json b/cli/html-report/tsconfig.json new file mode 100644 index 000000000..7aec7cdb8 --- /dev/null +++ b/cli/html-report/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "esModuleInterop": true + }, + "include": ["src", "setup-test.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/cli/html-report/tsconfig.node.json b/cli/html-report/tsconfig.node.json new file mode 100644 index 000000000..97ede7ee6 --- /dev/null +++ b/cli/html-report/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/cli/html-report/vite.config.ts b/cli/html-report/vite.config.ts new file mode 100644 index 000000000..16b5a784d --- /dev/null +++ b/cli/html-report/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react-swc" +import { viteSingleFile } from "vite-plugin-singlefile" +import path from "path" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), viteSingleFile()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + build: { + outDir: "../resources", + emptyOutDir: false, + rollupOptions: { + input: "report.html", + }, + }, + server: { + open: "report.html", + }, +}) diff --git a/cli/jest.config.js b/cli/jest.config.js index 294fd8eb1..db1001fc7 100644 --- a/cli/jest.config.js +++ b/cli/jest.config.js @@ -2,5 +2,6 @@ export default { transform: {'^.+\\.ts?$': 'ts-jest'}, testEnvironment: 'node', testRegex: '/src/.+\\.test?\\.(ts|tsx)$', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + modulePathIgnorePatterns: ['html-report'] } diff --git a/cli/src/analyze.ts b/cli/src/analyze.ts index d11c8a190..e217772e8 100644 --- a/cli/src/analyze.ts +++ b/cli/src/analyze.ts @@ -1,268 +1,523 @@ -import chalk from "chalk"; +import chalk from "chalk" import { - dateFromIsoFilename, - dateToIsoFilename, - emailParse, - groupBy, - isNotUndefined, - Logger, - mapValues, - partition, - pathJoin, - pluralize, - pluralizeL, - publicEmailDomains, - removeEmpty, - removeUndefined -} from "@azimutt/utils"; + dateFromIsoFilename, + dateToIsoFilename, + emailParse, + groupBy, + isNotUndefined, + Logger, + mapValues, + partition, + pathJoin, + pluralize, + pluralizeL, + publicEmailDomains, + removeEmpty, + removeUndefined, +} from "@azimutt/utils" import { - analyzeDatabase, - AnalyzeHistory, - AnalyzeReport, - AnalyzeReportRule, - azimuttEmail, - Connector, - Database, - DatabaseQuery, - DatabaseUrlParsed, - parseDatabaseUrl, - RuleAnalyzed, - RuleId, - RuleLevel, - ruleLevelsShown, - RulesConf, - zodParse, - zodParseAsync -} from "@azimutt/models"; -import {getConnector, track} from "@azimutt/gateway"; -import {version} from "./version.js"; -import {loggerNoOp} from "./utils/logger.js"; -import {fileExists, fileList, fileReadJson, fileWriteJson, mkParentDirs} from "./utils/file.js"; + analyzeDatabase, + AnalyzeHistory, + AnalyzeReportLevel, + AnalyzeReport, + AnalyzeReportRule, + azimuttEmail, + Connector, + Database, + DatabaseQuery, + DatabaseUrlParsed, + parseDatabaseUrl, + RuleAnalyzed, + RuleId, + RuleLevel, + ruleLevelsShown, + RulesConf, + zodParse, + zodParseAsync, + AnalyzeReportHtmlResult, + AnalyzeStats, +} from "@azimutt/models" +import { getConnector, track } from "@azimutt/gateway" +import { version } from "./version.js" +import { loggerNoOp } from "./utils/logger.js" +import { + fileExists, + fileList, + fileRead, + fileReadJson, + fileWrite, + fileWriteJson, + mkParentDirs, +} from "./utils/file.js" export type Opts = { - folder?: string - email?: string - size?: number - only?: string - key?: string - ignoreViolationsFrom?: string + folder?: string + email?: string + size?: number + only?: string + key?: string + ignoreViolationsFrom?: string + html?: boolean } -export async function launchAnalyze(url: string, opts: Opts, logger: Logger): Promise { - const dbUrl: DatabaseUrlParsed = parseDatabaseUrl(url) - const connector: Connector | undefined = getConnector(dbUrl) - if (!connector) return Promise.reject(`Invalid connector for ${dbUrl.kind ? `${dbUrl.kind} db` : `unknown db (${dbUrl.full})`}`) - if (opts.email && !isValidEmail(dbUrl, opts.email, logger)) return Promise.reject(`Invalid email (${opts.email})`) - if (opts.key && !isValidKey(dbUrl, opts.email, opts.key, logger)) return Promise.reject(`Invalid key (${opts.key})`) - if (opts.ignoreViolationsFrom && !opts.key) return Promise.reject(`You need a 'key' to ignore violations from a report`) +export async function launchAnalyze( + url: string, + opts: Opts, + logger: Logger +): Promise { + const dbUrl: DatabaseUrlParsed = parseDatabaseUrl(url) + const connector: Connector | undefined = getConnector(dbUrl) + if (!connector) + return Promise.reject( + `Invalid connector for ${dbUrl.kind ? `${dbUrl.kind} db` : `unknown db (${dbUrl.full})`}` + ) + if (opts.email && !isValidEmail(dbUrl, opts.email, logger)) + return Promise.reject(`Invalid email (${opts.email})`) + if (opts.key && !isValidKey(dbUrl, opts.email, opts.key, logger)) + return Promise.reject(`Invalid key (${opts.key})`) + if (opts.ignoreViolationsFrom && !opts.key) + return Promise.reject(`You need a 'key' to ignore violations from a report`) - // TODO: extend config for user, database, queries, data... ({user: {}, database: {}, queries: {}, data: {}, rules: {}}) - const app = 'azimutt-analyze' - const folder = opts.folder || `~/.azimutt/analyze${dbUrl.db ? '/' + dbUrl.db : ''}` - const now = Date.now() - const conf: RulesConf = await loadConf(folder, logger) - const history = opts.key ? await loadHistory(folder, logger) : [] - const referenceReport: AnalyzeReport | undefined = opts.ignoreViolationsFrom ? await loadReferenceReport(folder, opts.ignoreViolationsFrom, logger) : undefined - const connectorLogger = conf.database?.logQueries ? logger : loggerNoOp - const db: Database = await connector.getSchema(app, dbUrl, {...conf.database, logger: connectorLogger}) - const queries: DatabaseQuery[] = await connector.getQueryHistory(app, dbUrl, {database: dbUrl.db, logger: connectorLogger}).catch(err => { - if (typeof err === 'string' && err === 'Not implemented') logger.log(chalk.blue(`Query history is not supported yet on ${dbUrl.kind}, ping us ;)`)) - if (typeof err === 'object' && 'message' in err && err.message.indexOf('"pg_stat_statements" does not exist')) logger.log(chalk.blue(`Can't get query history as pg_stat_statements is not enabled. Enable it for a better db analysis.`)) - return [] + // TODO: extend config for user, database, queries, data... ({user: {}, database: {}, queries: {}, data: {}, rules: {}}) + const app = "azimutt-analyze" + const folder = + opts.folder || `~/.azimutt/analyze${dbUrl.db ? "/" + dbUrl.db : ""}` + const now = Date.now() + const conf: RulesConf = await loadConf(folder, logger) + const history = opts.key ? await loadHistory(folder, logger) : [] + const referenceReport: AnalyzeReport | undefined = opts.ignoreViolationsFrom + ? await loadReferenceReport(folder, opts.ignoreViolationsFrom, logger) + : undefined + const connectorLogger = conf.database?.logQueries ? logger : loggerNoOp + const db: Database = await connector.getSchema(app, dbUrl, { + ...conf.database, + logger: connectorLogger, + }) + const queries: DatabaseQuery[] = await connector + .getQueryHistory(app, dbUrl, { + database: dbUrl.db, + logger: connectorLogger, + }) + .catch((err) => { + if (typeof err === "string" && err === "Not implemented") + logger.log( + chalk.blue( + `Query history is not supported yet on ${dbUrl.kind}, ping us ;)` + ) + ) + if ( + typeof err === "object" && + "message" in err && + err.message.indexOf('"pg_stat_statements" does not exist') + ) + logger.log( + chalk.blue( + `Can't get query history as pg_stat_statements is not enabled. Enable it for a better db analysis.` + ) + ) + return [] }) - const rules: Record = analyzeDatabase(conf, now, db, queries, history, referenceReport?.analysis, opts.only?.split(',') || []) - const [offRules, usedRules] = partition(Object.values(rules), r => r.conf.level === RuleLevel.enum.off) - const rulesByLevel: Record = groupBy(usedRules, r => r.conf.level) - const stats = buildStats(db, queries, rulesByLevel) - track('cli__analyze__run', removeUndefined({version, database: dbUrl.kind, ...stats, email: opts.email, key: opts.key}), 'cli').then(() => {}) - await updateConf(folder, conf, rules) + const rules: Record = analyzeDatabase( + conf, + now, + db, + queries, + history, + referenceReport?.analysis, + opts.only?.split(",") || [] + ) + const [offRules, usedRules] = partition( + Object.values(rules), + (r) => r.conf.level === RuleLevel.enum.off + ) + const rulesByLevel: Record = groupBy( + usedRules, + (r) => r.conf.level + ) + const stats = buildStats(db, queries, rulesByLevel) + track( + "cli__analyze__run", + removeUndefined({ + version, + database: dbUrl.kind, + ...stats, + email: opts.email, + key: opts.key, + }), + "cli" + ).then(() => {}) + await updateConf(folder, conf, rules) - if (opts.email) { - const maxShown = opts.size || 3 - printReport(offRules, rulesByLevel, maxShown, stats, logger) - const report = buildReport(db, queries, rules) - await writeReport(folder, report, logger) - if (opts.key) { - logger.log(chalk.blue('Thanks for using Azimutt analyze!')) - logger.log(chalk.blue(`For any question or suggestion, reach out to ${azimuttEmail}.`)) - logger.log(chalk.blue(`Cheers!`)) - logger.log('') - } else { - logger.log(chalk.blue('Hope you like Azimutt analyze!')) - logger.log(chalk.blue('Get even more from it with a license key, enabling historical analysis to identify:')) - logger.log(chalk.blue('- degrading queries')) - logger.log(chalk.blue('- unused indexes')) - logger.log(chalk.blue('- fastest growing tables')) - logger.log(chalk.blue('- and more...')) - logger.log(chalk.blue(`Reach out to ${azimuttEmail} to buy one.`)) - logger.log(chalk.blue(`See you ;)`)) - logger.log('') - } + const maxShown = opts.email ? opts.size ?? 3 : 3 + let report: AnalyzeReport | null = null + if (opts.email) { + printReport(offRules, rulesByLevel, maxShown, stats, logger) + report = buildReport(db, queries, rules) + await writeReport(folder, report, logger) + if (opts.key) { + logger.log(chalk.blue("Thanks for using Azimutt analyze!")) + logger.log( + chalk.blue( + `For any question or suggestion, reach out to ${azimuttEmail}.` + ) + ) + logger.log(chalk.blue(`Cheers!`)) + logger.log("") } else { - const maxShown = 3 - printReport(offRules, rulesByLevel, maxShown, stats, logger) - logger.log(chalk.blue('Had useful insights using Azimutt analyze?')) - logger.log(chalk.blue('Add your professional email (ex: `--email your.name@company.com`) to get the full report in JSON.')) - logger.log(chalk.blue(`Reach out to ${azimuttEmail} for feedback or suggest improvements ;)`)) - logger.log(chalk.blue(`Cheers!`)) - logger.log('') + logger.log(chalk.blue("Hope you like Azimutt analyze!")) + logger.log( + chalk.blue( + "Get even more from it with a license key, enabling historical analysis to identify:" + ) + ) + logger.log(chalk.blue("- degrading queries")) + logger.log(chalk.blue("- unused indexes")) + logger.log(chalk.blue("- fastest growing tables")) + logger.log(chalk.blue("- and more...")) + logger.log(chalk.blue(`Reach out to ${azimuttEmail} to buy one.`)) + logger.log(chalk.blue(`See you ;)`)) + logger.log("") } + } else { + printReport(offRules, rulesByLevel, maxShown, stats, logger) + logger.log(chalk.blue("Had useful insights using Azimutt analyze?")) + logger.log( + chalk.blue( + "Add your professional email (ex: `--email your.name@company.com`) to get the full report in JSON." + ) + ) + logger.log( + chalk.blue( + `Reach out to ${azimuttEmail} for feedback or suggest improvements ;)` + ) + ) + logger.log(chalk.blue(`Cheers!`)) + logger.log("") + } + + if (opts.html) { + if (!report) report = buildReport(db, queries, rules) + await writeHtmlReport(folder, report, stats, maxShown, logger) + } } -function isValidEmail(dbUrl: DatabaseUrlParsed, email: string, logger: Logger): boolean { - const parsed = emailParse(email.trim()) - if (parsed.domain) { - if (parsed.domain === 'azimutt.app') { - logger.log(chalk.red(`Do you really have an 'azimutt.app' email? Good try ;)`)) - return false - } else if (publicEmailDomains.includes(parsed.domain)) { - track('cli__analyze__run', removeUndefined({version, database: dbUrl.kind, email, error: 'wrong email'}), 'cli').then(() => {}) - logger.log(chalk.red(`Got email param, please use your professional one instead ;)`)) - return false - } else { - return true - } +function isValidEmail( + dbUrl: DatabaseUrlParsed, + email: string, + logger: Logger +): boolean { + const parsed = emailParse(email.trim()) + if (parsed.domain) { + if (parsed.domain === "azimutt.app") { + logger.log( + chalk.red(`Do you really have an 'azimutt.app' email? Good try ;)`) + ) + return false + } else if (publicEmailDomains.includes(parsed.domain)) { + track( + "cli__analyze__run", + removeUndefined({ + version, + database: dbUrl.kind, + email, + error: "wrong email", + }), + "cli" + ).then(() => {}) + logger.log( + chalk.red( + `Got email param, please use your professional one instead ;)` + ) + ) + return false } else { - logger.log(chalk.red(`Unrecognized email (${email}), try adding quotes around it.`)) - return false + return true } + } else { + logger.log( + chalk.red(`Unrecognized email (${email}), try adding quotes around it.`) + ) + return false + } } -function isValidKey(dbUrl: DatabaseUrlParsed, email: string | undefined, key: string, logger: Logger): boolean { - if (!email) { - logger.log(chalk.red(`You must provide your email alongside your key.`)) - return false - } else if (key !== 'sesame') { - logger.log(chalk.red(`Unrecognized key for ${email}, reach out to ${azimuttEmail} for help.`)) - track('cli__analyze__run', removeUndefined({version, database: dbUrl.kind, email, key, error: 'wrong key'}), 'cli').then(() => {}) - return false - } else { - return true - } +function isValidKey( + dbUrl: DatabaseUrlParsed, + email: string | undefined, + key: string, + logger: Logger +): boolean { + if (!email) { + logger.log(chalk.red(`You must provide your email alongside your key.`)) + return false + } else if (key !== "sesame") { + logger.log( + chalk.red( + `Unrecognized key for ${email}, reach out to ${azimuttEmail} for help.` + ) + ) + track( + "cli__analyze__run", + removeUndefined({ + version, + database: dbUrl.kind, + email, + key, + error: "wrong key", + }), + "cli" + ).then(() => {}) + return false + } else { + return true + } } -const confPath = (folder: string): string => pathJoin(`${folder}`, 'conf.json') +const confPath = (folder: string): string => pathJoin(`${folder}`, "conf.json") async function loadConf(folder: string, logger: Logger): Promise { - const path = confPath(folder) - if (fileExists(path)) { - logger.log(`Loading conf from ${path}`) - return await fileReadJson(path).then(zodParseAsync(RulesConf, `RulesConf reading ${path}`)) - } else { - mkParentDirs(path) - const conf: RulesConf = {} // initial conf - await fileWriteJson(path, conf) - return conf - } + const path = confPath(folder) + if (fileExists(path)) { + logger.log(`Loading conf from ${path}`) + return await fileReadJson(path).then( + zodParseAsync(RulesConf, `RulesConf reading ${path}`) + ) + } else { + mkParentDirs(path) + const conf: RulesConf = {} // initial conf + await fileWriteJson(path, conf) + return conf + } } -async function updateConf(folder: string, conf: RulesConf, rules: Record): Promise { - const path = confPath(folder) - const usedConf: RulesConf = removeEmpty({ - ...conf, - rules: Object.entries(rules).reduce((c, [id, {conf}]) => Object.assign(c, {[id]: conf}), conf.rules || {}) - }) - await fileWriteJson(path, usedConf) +async function updateConf( + folder: string, + conf: RulesConf, + rules: Record +): Promise { + const path = confPath(folder) + const usedConf: RulesConf = removeEmpty({ + ...conf, + rules: Object.entries(rules).reduce( + (c, [id, { conf }]) => Object.assign(c, { [id]: conf }), + conf.rules || {} + ), + }) + await fileWriteJson(path, usedConf) } -type AnalyzeStats = { - nb_entities: number, - nb_relations: number, - nb_queries: number, - nb_types: number, - nb_rules: number, - nb_violations: number, - violations: Record, +function buildStats( + db: Database, + queries: DatabaseQuery[], + rulesByLevel: Record +): AnalyzeStats { + const violationsByLevel: Record = mapValues( + rulesByLevel, + (rules) => rules.reduce((acc, rule) => acc + rule.violations.length, 0) + ) + return { + nb_entities: db.entities?.length || 0, + nb_relations: db.relations?.length || 0, + nb_queries: queries.length, + nb_types: db.types?.length || 0, + nb_rules: Object.values(rulesByLevel).reduce( + (acc, rules) => acc + rules.length, + 0 + ), + nb_violations: Object.values(violationsByLevel).reduce( + (acc, count) => acc + count, + 0 + ), + violations: violationsByLevel, + } } -function buildStats(db: Database, queries: DatabaseQuery[], rulesByLevel: Record): AnalyzeStats { - const violationsByLevel: Record = mapValues(rulesByLevel, rules => rules.reduce((acc, rule) => acc + rule.violations.length, 0)) - return { - nb_entities: db.entities?.length || 0, - nb_relations: db.relations?.length || 0, - nb_queries: queries.length, - nb_types: db.types?.length || 0, - nb_rules: Object.values(rulesByLevel).reduce((acc, rules) => acc + rules.length, 0), - nb_violations: Object.values(violationsByLevel).reduce((acc, count) => acc + count, 0), - violations: violationsByLevel - } +function ruleIgnores(rule: RuleAnalyzed): string { + return "ignores" in rule.conf && Array.isArray(rule.conf.ignores) + ? ` (${pluralize(rule.conf.ignores.length, "ignore")})` + : "" } -function printReport(offRules: RuleAnalyzed[], rulesByLevel: Record, maxShown: number, stats: AnalyzeStats, logger: Logger): void { - logger.log('') - if (offRules.length > 0) { - logger.log(`${pluralizeL(offRules, 'off rule')}: ${offRules.map(r => r.rule.name).join(', ')}`) - } - ruleLevelsShown.slice().reverse().forEach(level => { - const levelRules = rulesByLevel[level] || [] - const levelViolationsCount = levelRules.reduce((acc, r) => acc + r.violations.length, 0) - logger.log(`${levelViolationsCount} ${level} violations (${pluralizeL(levelRules, 'rule')}):`) - levelRules.forEach(rule => { - const ignores = 'ignores' in rule.conf && Array.isArray(rule.conf.ignores) ? ` (${pluralize(rule.conf.ignores.length, 'ignore')})` : '' - logger.log(` ${rule.violations.length} ${rule.rule.name}${ignores}${rule.violations.length > 0 ? ':' : ''}`) - rule.violations.slice(0, maxShown).forEach(violation => { - logger.log(` - ${violation.message}`) - }) - if (rule.violations.length > maxShown) { - logger.log(` ... ${rule.violations.length - maxShown} more`) - } +function printReport( + offRules: RuleAnalyzed[], + rulesByLevel: Record, + maxShown: number, + stats: AnalyzeStats, + logger: Logger +): void { + logger.log("") + if (offRules.length > 0) { + logger.log( + `${pluralizeL(offRules, "off rule")}: ${offRules.map((r) => r.rule.name).join(", ")}` + ) + } + ruleLevelsShown + .slice() + .reverse() + .forEach((level) => { + const levelRules = rulesByLevel[level] || [] + const levelViolationsCount = levelRules.reduce( + (acc, r) => acc + r.violations.length, + 0 + ) + logger.log( + `${levelViolationsCount} ${level} violations (${pluralizeL(levelRules, "rule")}):` + ) + levelRules.forEach((rule) => { + const ignores = ruleIgnores(rule) + logger.log( + ` ${rule.violations.length} ${rule.rule.name}${ignores}${rule.violations.length > 0 ? ":" : ""}` + ) + rule.violations.slice(0, maxShown).forEach((violation) => { + logger.log(` - ${violation.message}`) }) + if (rule.violations.length > maxShown) { + logger.log(` ... ${rule.violations.length - maxShown} more`) + } + }) }) - logger.log('') - logger.log(`Found ${pluralize(stats.nb_entities, 'entity')}, ${pluralize(stats.nb_relations, 'relation')}, ${pluralize(stats.nb_queries, 'query')} and ${pluralize(stats.nb_types, 'type')} on the database.`) - logger.log(`Found ${stats.nb_violations} violations using ${stats.nb_rules} rules: ${ruleLevelsShown.map(l => `${(stats.violations[l] || 0)} ${l}`).join(', ')}.`) - logger.log('') + logger.log("") + logger.log( + `Found ${pluralize(stats.nb_entities, "entity")}, ${pluralize(stats.nb_relations, "relation")}, ${pluralize(stats.nb_queries, "query")} and ${pluralize(stats.nb_types, "type")} on the database.` + ) + logger.log( + `Found ${stats.nb_violations} violations using ${stats.nb_rules} rules: ${ruleLevelsShown.map((l) => `${stats.violations[l] || 0} ${l}`).join(", ")}.` + ) + logger.log("") } -function buildReport(database: Database, queries: DatabaseQuery[], rules: Record): AnalyzeReport { - return zodParse(AnalyzeReport)({ - analysis: Object.fromEntries(Object.entries(rules) - .filter(([, r]) => r.violations.length > 0) - .map(([id, r]) => [id, buildRuleReport(r)])), - database, - queries, - }).getOrThrow() +function buildReport( + database: Database, + queries: DatabaseQuery[], + rules: Record +): AnalyzeReport { + return zodParse(AnalyzeReport)({ + analysis: Object.fromEntries( + Object.entries(rules) + .filter(([, r]) => r.violations.length > 0) + .map(([id, r]) => [id, buildRuleReport(r)]) + ), + database, + queries, + }).getOrThrow() } function buildRuleReport(rule: RuleAnalyzed): AnalyzeReportRule { - const {level, ...conf} = rule.conf - const violations = rule.violations.map(v => removeUndefined({ - message: v.message, - entity: v.entity, - attribute: v.attribute, - extra: v.extra, - })) - return {name: rule.rule.name, level, conf, violations} + const { level, ...conf } = rule.conf + const violations = rule.violations.map((v) => + removeUndefined({ + message: v.message, + entity: v.entity, + attribute: v.attribute, + extra: v.extra, + }) + ) + return { + name: rule.rule.name, + level, + conf, + violations, + totalViolations: violations.length, + } } -async function writeReport(folder: string, report: AnalyzeReport, logger: Logger): Promise { - const path = pathJoin(folder, `report_${dateToIsoFilename(new Date())}.azimutt.json`) - await fileWriteJson(path, report) - logger.log(`Analysis report written to ${path}`) - logger.log('') +async function writeReport( + folder: string, + report: AnalyzeReport, + logger: Logger +): Promise { + const path = pathJoin( + folder, + `report_${dateToIsoFilename(new Date())}.azimutt.json` + ) + await fileWriteJson(path, report) + logger.log(`Analysis report written to ${path}`) + logger.log("") } -async function loadReferenceReport(folder: string, report: string, logger: Logger): Promise { - const path = pathJoin(folder, report) - const res = await fileReadJson(path).then(zodParseAsync(AnalyzeReport)) - logger.log(`Loaded reference report from ${path}`) - return res +async function writeHtmlReport( + folder: string, + report: AnalyzeReport, + stats: AnalyzeStats, + maxShown: number, + logger: Logger +): Promise { + const path = pathJoin( + folder, + `report_${dateToIsoFilename(new Date())}.azimutt.html` + ) + + const { analysis } = report + + const rules = Object.values(analysis).map(({ violations, ...rule }) => ({ + ...rule, + violations: violations.slice(0, maxShown), + })) + + const { nb_entities, nb_relations, nb_queries, nb_types, nb_rules } = stats + + const reportResult: AnalyzeReportHtmlResult = { + rules, + stats: { + nb_entities, + nb_relations, + nb_queries, + nb_types, + nb_rules, + }, + } + + let html = await fileRead("./resources/report.html") + html = html.replace( + '', + `` + ) + await fileWrite(path, html) + logger.log(`Analysis report written to ${path}`) + logger.log("") } -async function loadHistory(folder: string, logger: Logger): Promise { - const files = await fileList(folder) - const history = files - .map(file => { - const [, date] = file.match(/^report_([0-9-TZ]{24})\.azimutt\.json$/) || [] - return date ? {date: dateFromIsoFilename(date).getTime(), path: pathJoin(folder, file)} : undefined - }) - .filter(isNotUndefined) - .map(({date, path}) => - fileReadJson(path) - .then(zodParseAsync(AnalyzeReport)) - .then(report => ({report: path, date, database: report.database, queries: report.queries})) - ) - const res = await Promise.all(history) - logger.log(`Loaded ${pluralizeL(res, 'previous report')} from ${folder}`) - return res +async function loadReferenceReport( + folder: string, + report: string, + logger: Logger +): Promise { + const path = pathJoin(folder, report) + const res = await fileReadJson(path).then( + zodParseAsync(AnalyzeReport) + ) + logger.log(`Loaded reference report from ${path}`) + return res +} + +async function loadHistory( + folder: string, + logger: Logger +): Promise { + const files = await fileList(folder) + const history = files + .map((file) => { + const [, date] = + file.match(/^report_([0-9-TZ]{24})\.azimutt\.json$/) || [] + return date + ? { + date: dateFromIsoFilename(date).getTime(), + path: pathJoin(folder, file), + } + : undefined + }) + .filter(isNotUndefined) + .map(({ date, path }) => + fileReadJson(path) + .then(zodParseAsync(AnalyzeReport)) + .then((report) => ({ + report: path, + date, + database: report.database, + queries: report.queries, + })) + ) + const res = await Promise.all(history) + logger.log(`Loaded ${pluralizeL(res, "previous report")} from ${folder}`) + return res } diff --git a/cli/src/index.ts b/cli/src/index.ts index 9de66682a..91eeda7b5 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -48,6 +48,7 @@ program.command('analyze') .option('--only ', 'limit analyze to specified rules') .option('--key ', `reach out to ${azimuttEmail} to buy a key for incremental rules: degrading queries, unused tables/indexes, fast growing tables/indexes and more...`) .option('--ignore-violations-from ', 'ignore violations present in existing report, path is relative to report folder, needs --key argument') + .option('--html', 'get full analyze report as a HTML file') .action((url, args) => exec(launchAnalyze(url, args, logger), args)) program.command('export') diff --git a/cli/src/utils/file.ts b/cli/src/utils/file.ts index 67f5fc695..2fc0d2324 100644 --- a/cli/src/utils/file.ts +++ b/cli/src/utils/file.ts @@ -1,6 +1,8 @@ import * as fs from "node:fs"; import os from "os"; import {pathParent} from "@azimutt/utils"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; export type FilePath = string export type FileFormat = 'json' | 'sql' @@ -9,8 +11,12 @@ export const mkParentDirs = (path: string): void => { fs.mkdirSync(pathResolve(p export const fileExists = (path: string): boolean => fs.existsSync(pathResolve(path)) export const fileList = (path: string): Promise => fs.promises.readdir(pathResolve(path)) export const fileReadJson = (path: string): Promise => fs.promises.readFile(pathResolve(path)).then(str => JSON.parse(str.toString())) +export const fileRead = (path: string): Promise => fs.promises.readFile(pathResolve(path)).then(str => str.toString()) export const fileWriteJson = (path: string, json: T): Promise => fs.promises.writeFile(pathResolve(path), JSON.stringify(json, null, 2) + '\n') export const fileWrite = (path: string, content: string): Promise => fs.promises.writeFile(pathResolve(path), content) export const userHome = (): string => os.homedir() export const pathResolve = (path: string): string => path.startsWith('~/') ? path.replace(/^~/, userHome()) : path + +export const __filename = fileURLToPath(import.meta.url); +export const __dirname = dirname(__filename); diff --git a/gateway/src/services/tracking.ts b/gateway/src/services/tracking.ts index ea80e4860..744929a64 100644 --- a/gateway/src/services/tracking.ts +++ b/gateway/src/services/tracking.ts @@ -16,4 +16,4 @@ export async function track(name: string, details: object, instance: string, env createdAt: new Date().toISOString() }) }).then(() => {}, () => {}) -} +} \ No newline at end of file diff --git a/libs/models/src/analyze/rule.ts b/libs/models/src/analyze/rule.ts index 57f8e857d..e428dc5f5 100644 --- a/libs/models/src/analyze/rule.ts +++ b/libs/models/src/analyze/rule.ts @@ -1,28 +1,41 @@ -import {z, ZodType} from "zod"; -import {Timestamp} from "../common"; -import {AttributePath, Database, EntityRef} from "../database"; -import {DatabaseQuery} from "../interfaces/connector"; +import { number, z, ZodType } from "zod" +import { Timestamp } from "../common" +import { AttributePath, Database, EntityRef } from "../database" +import { DatabaseQuery } from "../interfaces/connector" export interface Rule { - id: RuleId - name: RuleName - conf: Conf - zConf: ZodType - analyze(conf: Conf, now: Timestamp, db: Database, queries: DatabaseQuery[], history: AnalyzeHistory[], reference: AnalyzeReportViolation[]): RuleViolation[] + id: RuleId + name: RuleName + conf: Conf + zConf: ZodType + analyze( + conf: Conf, + now: Timestamp, + db: Database, + queries: DatabaseQuery[], + history: AnalyzeHistory[], + reference: AnalyzeReportViolation[] + ): RuleViolation[] } export const RuleId = z.string() export type RuleId = z.infer export const RuleName = z.string() export type RuleName = z.infer -export const RuleLevel = z.enum(['high', 'medium', 'low', 'hint', 'off']) // from highest to lowest +export const RuleLevel = z.enum(["high", "medium", "low", "hint", "off"]) // from highest to lowest export type RuleLevel = z.infer -export const ruleLevelsShown = RuleLevel.options.filter(l => l !== RuleLevel.enum.off) -export const RuleConf = z.object({ - level: RuleLevel -}).strict().describe('RuleConf') +export const ruleLevelsShown = RuleLevel.options.filter( + (l) => l !== RuleLevel.enum.off +) +export const RuleConf = z + .object({ + level: RuleLevel, + }) + .strict() + .describe("RuleConf") export type RuleConf = z.infer -export const RuleViolation = z.object({ +export const RuleViolation = z + .object({ ruleId: RuleId, ruleName: RuleName, ruleLevel: RuleLevel, @@ -31,41 +44,97 @@ export const RuleViolation = z.object({ entity: EntityRef.optional(), attribute: AttributePath.optional(), // extra allow to keep structured information about the violation - extra: z.record(z.any()).optional() -}).strict() + extra: z.record(z.any()).optional(), + }) + .strict() export type RuleViolation = z.infer -export const AnalyzeReportViolation = z.object({ +export const AnalyzeReportViolation = z + .object({ message: z.string(), entity: EntityRef.optional(), attribute: AttributePath.optional(), extra: z.record(z.any()).optional(), -}).strict().describe('AnalyzeReportViolation') + }) + .strict() + .describe("AnalyzeReportViolation") export type AnalyzeReportViolation = z.infer -export const AnalyzeReportRule = z.object({ +export const AnalyzeReportRule = z + .object({ name: z.string(), level: RuleLevel, conf: z.record(z.string(), z.any()), - violations: AnalyzeReportViolation.array() -}).strict().describe('AnalyzeReportRule') + violations: AnalyzeReportViolation.array(), + totalViolations: z.number(), + }) + .strict() + .describe("AnalyzeReportRule") export type AnalyzeReportRule = z.infer +export const AnalyzeReportRuleSummary = z + .object({ + name: z.string(), + totalViolations: z.number(), + }) + .strict() + .describe("AnalyzeReportRuleSummary") +export type AnalyzeReportRuleSummary = z.infer + export const AnalyzeReportResult = z.record(RuleId, AnalyzeReportRule) export type AnalyzeReportResult = z.infer -export const AnalyzeReport = z.object({ +export const AnalyzeReport = z + .object({ analysis: AnalyzeReportResult, // TODO: insights: z.object({mostUsedEntities: z.object({entity: EntityRef, queries: QueryId.array()}).array().optional()}).optional() database: Database, queries: DatabaseQuery.array(), -}).strict().describe('AnalyzeReport') + }) + .strict() + .describe("AnalyzeReport") export type AnalyzeReport = z.infer -export const AnalyzeHistory = z.object({ +export const AnalyzeHistory = z + .object({ report: z.string(), date: Timestamp, database: Database, queries: DatabaseQuery.array(), -}).strict().describe('AnalyzeHistory') + }) + .strict() + .describe("AnalyzeHistory") export type AnalyzeHistory = z.infer + +export const AnalyzeReportLevel = z.object({ + level: RuleLevel, + levelViolationsCount: z.number(), + rules: z.array(AnalyzeReportRule), +}) +export type AnalyzeReportLevel = z.infer + +export const AnalyzeStats = z.object({ + nb_entities: z.number(), + nb_relations: z.number(), + nb_queries: z.number(), + nb_types: z.number(), + nb_rules: z.number(), + nb_violations: z.number(), + violations: z.record(RuleLevel, z.number()), +}) +export type AnalyzeStats = z.infer + +export const AnalyzeReportHtmlStats = z.object({ + nb_entities: z.number(), + nb_relations: z.number(), + nb_queries: z.number(), + nb_types: z.number(), + nb_rules: z.number(), +}) +export type AnalyzeReportHtmlStats = z.infer + +export const AnalyzeReportHtmlResult = z.object({ + rules: z.array(AnalyzeReportRule), + stats: AnalyzeReportHtmlStats, +}) +export type AnalyzeReportHtmlResult = z.infer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e342bf56f..31ea0110d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,23 +77,141 @@ importers: version: 29.5.12 '@types/node': specifier: ^20.12.13 - version: 20.12.13 + version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) nodemon: specifier: ^3.1.2 - version: 3.1.2 + version: 3.1.4 ts-jest: specifier: ^29.1.4 - version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)))(typescript@5.4.5) + version: 29.1.5(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.12.13)(typescript@5.4.5) + version: 10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5) typescript: specifier: ^5.4.5 version: 5.4.5 + cli/html-report: + dependencies: + '@azimutt/models': + specifier: workspace:^ + version: link:../../libs/models + '@azimutt/utils': + specifier: workspace:^ + version: link:../../libs/utils + '@radix-ui/react-dialog': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.0.2 + version: 1.1.0(@types/react@18.3.3)(react@18.3.1) + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + lucide-react: + specifier: ^0.395.0 + version: 0.395.0(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + tailwind-merge: + specifier: ^2.3.0 + version: 2.3.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5))) + devDependencies: + '@testing-library/dom': + specifier: ^10.1.0 + version: 10.1.0 + '@testing-library/jest-dom': + specifier: ^6.4.6 + version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(vitest@1.6.0(@types/node@20.14.5)(terser@5.31.0)) + '@testing-library/react': + specifier: ^16.0.0 + version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.1.0) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/node': + specifier: ^20.14.5 + version: 20.14.5 + '@types/react': + specifier: ^18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + '@typescript-eslint/eslint-plugin': + specifier: ^7.2.0 + version: 7.13.1(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': + specifier: ^7.2.0 + version: 7.13.1(eslint@8.57.0)(typescript@5.4.5) + '@vitejs/plugin-react-swc': + specifier: ^3.5.0 + version: 3.7.0(vite@5.2.12(@types/node@20.14.5)(terser@5.31.0)) + autoprefixer: + specifier: ^10.4.19 + version: 10.4.19(postcss@8.4.38) + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.2(eslint@8.57.0) + eslint-plugin-react-refresh: + specifier: ^0.4.6 + version: 0.4.7(eslint@8.57.0) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) + postcss: + specifier: ^8.4.38 + version: 8.4.38 + tailwindcss: + specifier: ^3.4.4 + version: 3.4.4(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + ts-jest: + specifier: ^29.1.5 + version: 29.1.5(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) + typescript: + specifier: ^5.2.2 + version: 5.4.5 + vite: + specifier: ^5.2.0 + version: 5.2.12(@types/node@20.14.5)(terser@5.31.0) + vite-plugin-singlefile: + specifier: ^2.0.1 + version: 2.0.2(rollup@4.18.0)(vite@5.2.12(@types/node@20.14.5)(terser@5.31.0)) + desktop: dependencies: '@azimutt/connector-postgres': @@ -132,7 +250,7 @@ importers: version: 7.4.0 '@electron-forge/plugin-webpack': specifier: ^7.4.0 - version: 7.4.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + version: 7.4.0(@swc/core@1.6.3)(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@electron-forge/publisher-github': specifier: ^7.4.0 version: 7.4.0(encoding@0.1.13) @@ -150,7 +268,7 @@ importers: version: 1.7.4 css-loader: specifier: ^7.1.1 - version: 7.1.2(webpack@5.92.0) + version: 7.1.2(webpack@5.92.0(@swc/core@1.6.3)) electron: specifier: 29.3.0 version: 29.3.0 @@ -162,25 +280,25 @@ importers: version: 2.29.1(@typescript-eslint/parser@7.11.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) fork-ts-checker-webpack-plugin: specifier: ^9.0.2 - version: 9.0.2(typescript@5.4.5)(webpack@5.92.0) + version: 9.0.2(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.6.3)) node-loader: specifier: ^2.0.0 - version: 2.0.0(webpack@5.92.0) + version: 2.0.0(webpack@5.92.0(@swc/core@1.6.3)) style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.92.0) + version: 4.0.0(webpack@5.92.0(@swc/core@1.6.3)) ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.4.5)(webpack@5.92.0) + version: 9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.6.3)) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.5)(typescript@5.4.5) + version: 10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5) typescript: specifier: ^5.4.5 version: 5.4.5 webpack: specifier: ^5.92.0 - version: 5.92.0 + version: 5.92.0(@swc/core@1.6.3) frontend: dependencies: @@ -256,7 +374,7 @@ importers: version: 0.21.4 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -384,7 +502,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -415,7 +533,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -446,7 +564,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -477,7 +595,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -508,7 +626,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -545,7 +663,7 @@ importers: version: 8.11.6 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.3 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -579,7 +697,7 @@ importers: version: 1.6.24 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -613,7 +731,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -650,7 +768,7 @@ importers: version: 8.14.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.4 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -684,7 +802,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -718,7 +836,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -749,7 +867,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -786,7 +904,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.2 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -807,7 +925,7 @@ importers: version: 20.14.5 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) ts-jest: specifier: ^29.1.4 version: 29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(esbuild@0.21.4)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -817,6 +935,9 @@ importers: packages: + '@adobe/css-tools@4.4.0': + resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1968,6 +2089,21 @@ packages: '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@floating-ui/core@1.6.2': + resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} + + '@floating-ui/dom@1.6.5': + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} + + '@floating-ui/react-dom@2.1.0': + resolution: {integrity: sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.2': + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -2212,6 +2348,463 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.0.5': + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dialog@1.1.1': + resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.0.5': + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.0': + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-guards@1.1.0': + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.0.4': + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.1': + resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.0.4': + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.1': + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.0.1': + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.0': + resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.1.1': + resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.0': + resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rollup/rollup-android-arm-eabi@4.18.0': resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} cpu: [arm] @@ -2563,6 +3156,81 @@ packages: resolution: {integrity: sha512-+fEXJxGDLCoqRKVSmo0auGxaqbiCo+8oph+4auefYjaNxjOLKSY2MxVQfRzo65PaZv4fr+5lWg+au7vSuJJ/zw==} engines: {node: '>=16.0.0'} + '@swc/core-darwin-arm64@1.6.3': + resolution: {integrity: sha512-3r7cJf1BcE30iyF1rnOSKrEzIR+cqnyYSZvivrm62TZdXVsIjfXe1xulsKGxZgNeLY5erIu7ukvMvBvPhnQvqA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.6.3': + resolution: {integrity: sha512-8GLZ23IgVpF5xh2SbS5ZW/12/EEBuRU1hFOLB5rKERJU0y1RJ6YhDMf/FuOWhfHQcFM7TeedBwHIzaF+tdKKlw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.6.3': + resolution: {integrity: sha512-VQ/bduX7WhLOlGbJLMG7UH0LBehjjx43R4yuk55rjjJLqpvX5fQzMsWhQdIZ5vsc+4ORzdgtEAlpumTv6bsD1A==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.6.3': + resolution: {integrity: sha512-jHIQ/PCwtdDBIF/BiC5DochswuCAIW/T5skJ+eDMbta7+QtEnZCXTZWpT5ORoEY/gtsE2fjpOA4TS6fBBvXqUw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.6.3': + resolution: {integrity: sha512-gA6velEUD27Dwu0BlR9hCcFzkWq2YL2pDAU5qbgeuGhaMiUCBssfqTQB+2ctEnV+AZx+hSMJOHvtA+uFZjfRrw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.6.3': + resolution: {integrity: sha512-fy4qoBDr5I8r+ZNCZxs/oZcmu4j/8mtSud6Ka102DaSxEjNg0vfIdo9ITsVIPsofhUTmDKjQsPB2O7YUlJAioQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.6.3': + resolution: {integrity: sha512-c/twcMbq/Gpq47G+b3kWgoaCujpXO11aRgJx6am+CprvP4uNeBHEpQkxD+DQmdWFHisZd0i9GB8NG3e7L9Rz9Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.6.3': + resolution: {integrity: sha512-y6RxMtX45acReQmzkxcEfJscfBXce6QjuNgWQHHs9exA592BZzmolDUwgmAyjyvopz1lWX+KdymdZFKvuDSx4w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.6.3': + resolution: {integrity: sha512-41h7z3xgukl1HDDwhquaeOPSP1OWeHl+mWKnJVmmwd3ui/oowUDCO856qa6JagBgPSnAGfyXwv6vthuXwyCcWA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.6.3': + resolution: {integrity: sha512-//bnwo9b8Vp1ED06eXCHyGZ5xIpdkQgg2fuFDdtd1FITl7r5bdQh2ryRzPiKiGwgXZwZQitUshI4JeEX9IuW+Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.6.3': + resolution: {integrity: sha512-mZpei+LqE+AL+nwgERMQey9EJA9/yhHTN6nwbobH5GnSij/lhfTdGfAb1iumOrroqEcXbHUaK//7wOw7DjBGdA==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.8': + resolution: {integrity: sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -2583,6 +3251,52 @@ packages: '@tediousjs/connection-string@0.5.0': resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==} + '@testing-library/dom@10.1.0': + resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.4.6': + resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@jest/globals': '>= 28' + '@types/bun': latest + '@types/jest': '>= 28' + jest: '>= 28' + vitest: '>= 0.32' + peerDependenciesMeta: + '@jest/globals': + optional: true + '@types/bun': + optional: true + '@types/jest': + optional: true + jest: + optional: true + vitest: + optional: true + + '@testing-library/react@16.0.0': + resolution: {integrity: sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -2599,6 +3313,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2719,21 +3436,27 @@ packages: '@types/node@18.19.33': resolution: {integrity: sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==} - '@types/node@20.12.13': - resolution: {integrity: sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==} - '@types/node@20.14.5': resolution: {integrity: sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==} '@types/pg@8.11.6': resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@18.3.0': + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + + '@types/react@18.3.3': + resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/request@2.48.12': resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} @@ -2916,6 +3639,11 @@ packages: '@vercel/webpack-asset-relocator-loader@1.7.4': resolution: {integrity: sha512-RFFite6v51Qhj/eERru3qwUNCLybnceSChI5yiu9bhLpTemWbKPORAOExOgpO2W7IE/0UEh3aX6wTSHgDE/fdQ==} + '@vitejs/plugin-react-swc@3.7.0': + resolution: {integrity: sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==} + peerDependencies: + vite: ^4 || ^5 + '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} @@ -3153,6 +3881,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -3243,6 +3978,13 @@ packages: resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} engines: {node: '>=0.8'} + autoprefixer@10.4.19: + resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3502,6 +4244,10 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3560,6 +4306,9 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -3608,11 +4357,25 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cmake-js@7.3.0: resolution: {integrity: sha512-dXs2zq9WxrV87bpJ+WbnGKv8WUBXDw8blNiwNHoRe/it+ptscxhQHKB1SJXa1w+kocLMeP28Tk4/eTCezg4o+w==} engines: {node: '>= 14.15.0'} hasBin: true + cmdk@1.0.0: + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -3808,6 +4571,9 @@ packages: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3823,6 +4589,9 @@ packages: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -3974,6 +4743,10 @@ packages: deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.0.4: resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} @@ -3989,6 +4762,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} @@ -4028,6 +4804,12 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -4200,10 +4982,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.16.1: - resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.17.0: resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} engines: {node: '>=10.13.0'} @@ -4506,6 +5284,17 @@ packages: eslint-config-prettier: optional: true + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react-refresh@0.4.7: + resolution: {integrity: sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==} + peerDependencies: + eslint: '>=7' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4858,6 +5647,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -4969,6 +5761,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-package-info@1.0.0: resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} engines: {node: '>= 4.0'} @@ -5329,6 +6125,9 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -5977,6 +6776,10 @@ packages: long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -6005,6 +6808,15 @@ packages: resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} engines: {node: '>=16.14'} + lucide-react@0.395.0: + resolution: {integrity: sha512-6hzdNH5723A4FLaYZWpK50iyZH8iS2Jq5zuPRRotOFkhu6kxxJiebVdJ72tCR5XkiIeYFOU5NUawFZOac+VeYw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -6117,6 +6929,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -6375,8 +7191,8 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - nodemon@3.1.2: - resolution: {integrity: sha512-/Ib/kloefDy+N0iRTxIUzyGcdW9lzlnca2Jsa5w73bs3npXjg+WInmiX6VY13mIb6SykkthYX/U5t0ukryGqBw==} + nodemon@3.1.4: + resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} engines: {node: '>=10'} hasBin: true @@ -6392,6 +7208,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -6912,6 +7732,10 @@ packages: pretty-error@4.0.0: resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7019,9 +7843,61 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.7: + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + read-binary-file-arch@1.0.6: resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} hasBin: true @@ -7071,6 +7947,10 @@ packages: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -7257,6 +8137,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -7600,6 +8483,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -7678,11 +8565,24 @@ packages: resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==} engines: {node: '>=10.0.0'} + tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tailwindcss@3.4.3: resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@3.4.4: + resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} + engines: {node: '>=14.0.0'} + hasBin: true + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -7893,6 +8793,30 @@ packages: esbuild: optional: true + ts-jest@29.1.5: + resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + ts-loader@9.5.1: resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} engines: {node: '>=12.0.0'} @@ -8060,6 +8984,26 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + username@5.1.0: resolution: {integrity: sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==} engines: {node: '>=8'} @@ -8114,6 +9058,13 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-singlefile@2.0.2: + resolution: {integrity: sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.18.0 + vite: ^5.3.1 + vite@5.2.12: resolution: {integrity: sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8505,6 +9456,8 @@ packages: snapshots: + '@adobe/css-tools@4.4.0': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -8588,7 +9541,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/core': 3.582.0 - '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/middleware-bucket-endpoint': 3.577.0 '@aws-sdk/middleware-expect-continue': 3.577.0 '@aws-sdk/middleware-flexible-checksums': 3.577.0 @@ -8649,7 +9602,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/core': 3.582.0 - '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -8738,7 +9691,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) '@aws-sdk/core': 3.582.0 - '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -8807,13 +9760,13 @@ snapshots: '@smithy/util-stream': 3.0.1 tslib: 2.6.2 - '@aws-sdk/credential-provider-ini@3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0)': + '@aws-sdk/credential-provider-ini@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0))': dependencies: '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0)) - '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) + '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 '@smithy/property-provider': 3.0.0 @@ -8824,14 +9777,14 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0)': + '@aws-sdk/credential-provider-node@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0))': dependencies: '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-http': 3.582.0 - '@aws-sdk/credential-provider-ini': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-ini': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0)) - '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) + '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 '@smithy/property-provider': 3.0.0 @@ -8851,10 +9804,10 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/credential-provider-sso@3.583.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))': + '@aws-sdk/credential-provider-sso@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)': dependencies: '@aws-sdk/client-sso': 3.583.0 - '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0)) + '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 @@ -8864,7 +9817,7 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-web-identity@3.577.0(@aws-sdk/client-sts@3.583.0)': + '@aws-sdk/credential-provider-web-identity@3.577.0(@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0))': dependencies: '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/types': 3.577.0 @@ -8985,7 +9938,7 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0))': + '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.583.0)': dependencies: '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) '@aws-sdk/types': 3.577.0 @@ -9676,7 +10629,7 @@ snapshots: - bluebird - supports-color - '@electron-forge/plugin-webpack@7.4.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@electron-forge/plugin-webpack@7.4.0(@swc/core@1.6.3)(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@electron-forge/core-utils': 7.4.0 '@electron-forge/plugin-base': 7.4.0 @@ -9686,10 +10639,10 @@ snapshots: debug: 4.3.4(supports-color@5.5.0) fast-glob: 3.3.2 fs-extra: 10.1.0 - html-webpack-plugin: 5.6.0(webpack@5.92.0) + html-webpack-plugin: 5.6.0(webpack@5.92.0(@swc/core@1.6.3)) listr2: 7.0.2 - webpack: 5.92.0 - webpack-dev-server: 4.15.2(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10)(webpack@5.92.0) + webpack: 5.92.0(@swc/core@1.6.3) + webpack-dev-server: 4.15.2(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10)(webpack@5.92.0(@swc/core@1.6.3)) webpack-merge: 5.10.0 transitivePeerDependencies: - '@rspack/core' @@ -10186,6 +11139,23 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 + '@floating-ui/core@1.6.2': + dependencies: + '@floating-ui/utils': 0.2.2 + + '@floating-ui/dom@1.6.5': + dependencies: + '@floating-ui/core': 1.6.2 + '@floating-ui/utils': 0.2.2 + + '@floating-ui/react-dom@2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.2': {} + '@gar/promisify@1.1.3': {} '@google-cloud/bigquery@7.7.0(encoding@0.1.13)': @@ -10292,42 +11262,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.14.5 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.7 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -10341,7 +11276,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -10642,6 +11577,431 @@ snapshots: '@pkgr/core@0.1.1': {} + '@radix-ui/number@1.1.0': {} + + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.24.6 + + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-direction@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-select@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@radix-ui/rect@1.1.0': {} + '@rollup/rollup-android-arm-eabi@4.18.0': optional: true @@ -11109,6 +12469,58 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 + '@swc/core-darwin-arm64@1.6.3': + optional: true + + '@swc/core-darwin-x64@1.6.3': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.6.3': + optional: true + + '@swc/core-linux-arm64-gnu@1.6.3': + optional: true + + '@swc/core-linux-arm64-musl@1.6.3': + optional: true + + '@swc/core-linux-x64-gnu@1.6.3': + optional: true + + '@swc/core-linux-x64-musl@1.6.3': + optional: true + + '@swc/core-win32-arm64-msvc@1.6.3': + optional: true + + '@swc/core-win32-ia32-msvc@1.6.3': + optional: true + + '@swc/core-win32-x64-msvc@1.6.3': + optional: true + + '@swc/core@1.6.3': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.8 + optionalDependencies: + '@swc/core-darwin-arm64': 1.6.3 + '@swc/core-darwin-x64': 1.6.3 + '@swc/core-linux-arm-gnueabihf': 1.6.3 + '@swc/core-linux-arm64-gnu': 1.6.3 + '@swc/core-linux-arm64-musl': 1.6.3 + '@swc/core-linux-x64-gnu': 1.6.3 + '@swc/core-linux-x64-musl': 1.6.3 + '@swc/core-win32-arm64-msvc': 1.6.3 + '@swc/core-win32-ia32-msvc': 1.6.3 + '@swc/core-win32-x64-msvc': 1.6.3 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.8': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 @@ -11134,7 +12546,48 @@ snapshots: async: 3.2.5 simple-lru-cache: 0.0.2 - '@tediousjs/connection-string@0.5.0': {} + '@tediousjs/connection-string@0.5.0': {} + + '@testing-library/dom@10.1.0': + dependencies: + '@babel/code-frame': 7.24.6 + '@babel/runtime': 7.24.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)))(vitest@1.6.0(@types/node@20.14.5)(terser@5.31.0))': + dependencies: + '@adobe/css-tools': 4.4.0 + '@babel/runtime': 7.24.6 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + optionalDependencies: + '@jest/globals': 29.7.0 + '@types/jest': 29.5.12 + jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) + vitest: 1.6.0(@types/node@20.14.5)(jsdom@20.0.3)(terser@5.31.0) + + '@testing-library/react@16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.6 + '@testing-library/dom': 10.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.1.0)': + dependencies: + '@testing-library/dom': 10.1.0 '@tootallnate/once@2.0.0': {} @@ -11146,6 +12599,8 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.6 @@ -11306,10 +12761,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.12.13': - dependencies: - undici-types: 5.26.5 - '@types/node@20.14.5': dependencies: undici-types: 5.26.5 @@ -11320,10 +12771,21 @@ snapshots: pg-protocol: 1.6.1 pg-types: 4.0.2 + '@types/prop-types@15.7.12': {} + '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.0': + dependencies: + '@types/react': 18.3.3 + + '@types/react@18.3.3': + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + '@types/request@2.48.12': dependencies: '@types/caseless': 0.12.5 @@ -11566,6 +13028,13 @@ snapshots: dependencies: resolve: 1.22.8 + '@vitejs/plugin-react-swc@3.7.0(vite@5.2.12(@types/node@20.14.5)(terser@5.31.0))': + dependencies: + '@swc/core': 1.6.3 + vite: 5.2.12(@types/node@20.14.5)(terser@5.31.0) + transitivePeerDependencies: + - '@swc/helpers' + '@vitest/expect@1.6.0': dependencies: '@vitest/spy': 1.6.0 @@ -11824,6 +13293,14 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.6.2 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -11930,6 +13407,16 @@ snapshots: author-regex@1.0.0: {} + autoprefixer@10.4.19(postcss@8.4.38): + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001625 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -12265,6 +13752,11 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -12350,6 +13842,10 @@ snapshots: cjs-module-lexer@1.3.1: {} + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -12404,6 +13900,10 @@ snapshots: clone@1.0.4: {} + clsx@2.0.0: {} + + clsx@2.1.1: {} + cmake-js@7.3.0: dependencies: axios: 1.7.2(debug@4.3.4) @@ -12422,6 +13922,16 @@ snapshots: transitivePeerDependencies: - supports-color + cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + co@4.6.0: {} collect-v8-coverage@1.0.2: {} @@ -12554,28 +14064,13 @@ snapshots: transitivePeerDependencies: - supports-color - create-jest@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - create-jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): + create-jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -12626,7 +14121,7 @@ snapshots: crypt@0.0.2: {} - css-loader@7.1.2(webpack@5.92.0): + css-loader@7.1.2(webpack@5.92.0(@swc/core@1.6.3)): dependencies: icss-utils: 5.1.0(postcss@8.4.38) postcss: 8.4.38 @@ -12637,7 +14132,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.2 optionalDependencies: - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) css-select@4.3.0: dependencies: @@ -12649,6 +14144,8 @@ snapshots: css-what@6.1.0: {} + css.escape@1.5.1: {} + cssesc@3.0.0: {} cssom@0.3.8: {} @@ -12659,6 +14156,8 @@ snapshots: dependencies: cssom: 0.3.8 + csstype@3.1.3: {} + d@1.0.2: dependencies: es5-ext: 0.10.64 @@ -12782,6 +14281,8 @@ snapshots: deprecation@2.3.1: {} + dequal@2.0.3: {} + destroy@1.0.4: {} destroy@1.2.0: {} @@ -12790,6 +14291,8 @@ snapshots: detect-newline@3.1.0: {} + detect-node-es@1.1.0: {} + detect-node@2.1.0: {} didyoumean@1.2.2: {} @@ -12823,6 +14326,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -13145,11 +14652,6 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.16.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - enhanced-resolve@5.17.0: dependencies: graceful-fs: 4.2.11 @@ -13540,6 +15042,14 @@ snapshots: '@types/eslint': 8.56.10 eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-plugin-react-refresh@0.4.7(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -13980,7 +15490,7 @@ snapshots: forever-agent@0.6.1: {} - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.4.5)(webpack@5.92.0): + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.6.3)): dependencies: '@babel/code-frame': 7.24.6 chalk: 4.1.2 @@ -13995,7 +15505,7 @@ snapshots: semver: 7.6.2 tapable: 2.2.1 typescript: 5.4.5 - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) form-data-encoder@1.7.2: {} @@ -14024,6 +15534,8 @@ snapshots: forwarded@0.2.0: {} + fraction.js@4.3.7: {} + fresh@0.5.2: {} fs-extra@10.1.0: @@ -14164,6 +15676,8 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-nonce@1.0.1: {} + get-package-info@1.0.0: dependencies: bluebird: 3.7.2 @@ -14414,7 +15928,7 @@ snapshots: relateurl: 0.2.7 terser: 5.31.0 - html-webpack-plugin@5.6.0(webpack@5.92.0): + html-webpack-plugin@5.6.0(webpack@5.92.0(@swc/core@1.6.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -14422,7 +15936,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) htmlparser2@6.1.0: dependencies: @@ -14604,6 +16118,10 @@ snapshots: interpret@3.1.1: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -14845,35 +16363,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-cli@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): + jest-cli@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + create-jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -14883,69 +16382,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)): - dependencies: - '@babel/core': 7.24.6 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.6) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.7 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.12.13 - ts-node: 10.9.2(@types/node@20.12.13)(typescript@5.4.5) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)): - dependencies: - '@babel/core': 7.24.6 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.6) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.7 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.14.5 - ts-node: 10.9.2(@types/node@20.12.13)(typescript@5.4.5) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): + jest-config@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)): dependencies: '@babel/core': 7.24.6 '@jest/test-sequencer': 29.7.0 @@ -14971,7 +16408,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.14.5 - ts-node: 10.9.2(@types/node@20.14.5)(typescript@5.4.5) + ts-node: 10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -15212,24 +16649,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): + jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + jest-cli: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -15523,6 +16948,10 @@ snapshots: long@5.2.3: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + loupe@2.3.7: dependencies: get-func-name: 2.0.2 @@ -15548,6 +16977,12 @@ snapshots: lru-cache@8.0.5: {} + lucide-react@0.395.0(react@18.3.1): + dependencies: + react: 18.3.1 + + lz-string@1.5.0: {} + magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -15660,6 +17095,8 @@ snapshots: mimic-response@3.1.0: {} + min-indent@1.0.1: {} + mini-svg-data-uri@1.4.4: {} minimalistic-assert@1.0.1: {} @@ -15899,14 +17336,14 @@ snapshots: node-int64@0.4.0: {} - node-loader@2.0.0(webpack@5.92.0): + node-loader@2.0.0(webpack@5.92.0(@swc/core@1.6.3)): dependencies: loader-utils: 2.0.4 - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) node-releases@2.0.14: {} - nodemon@3.1.2: + nodemon@3.1.4: dependencies: chokidar: 3.6.0 debug: 4.3.4(supports-color@5.5.0) @@ -15932,6 +17369,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + normalize-url@6.1.0: {} npm-run-path@2.0.2: @@ -16369,7 +17808,7 @@ snapshots: yaml: 2.4.2 optionalDependencies: postcss: 8.4.38 - ts-node: 10.9.2(@types/node@20.14.5)(typescript@5.4.5) + ts-node: 10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5) postcss-modules-extract-imports@3.1.0(postcss@8.4.38): dependencies: @@ -16456,6 +17895,12 @@ snapshots: lodash: 4.17.21 renderkid: 3.0.0 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -16551,8 +17996,59 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@17.0.2: {} + react-is@18.3.1: {} + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 + + react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + + react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + + react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + read-binary-file-arch@1.0.6: dependencies: debug: 4.3.4(supports-color@5.5.0) @@ -16623,6 +18119,11 @@ snapshots: dependencies: resolve: 1.22.8 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + regenerator-runtime@0.14.1: {} regexp-to-ast@0.5.0: {} @@ -16839,6 +18340,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 @@ -17275,6 +18780,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -17291,9 +18800,9 @@ snapshots: stubs@3.0.0: {} - style-loader@4.0.0(webpack@5.92.0): + style-loader@4.0.0(webpack@5.92.0(@swc/core@1.6.3)): dependencies: - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) sucrase@3.35.0: dependencies: @@ -17353,6 +18862,14 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tailwind-merge@2.3.0: + dependencies: + '@babel/runtime': 7.24.6 + + tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5))): + dependencies: + tailwindcss: 3.4.4(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + tailwindcss@3.4.3(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): dependencies: '@alloc/quick-lru': 5.2.0 @@ -17380,6 +18897,33 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.7 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.1.0 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + tapable@2.2.1: {} tar@4.4.19: @@ -17445,14 +18989,16 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.10(webpack@5.92.0): + terser-webpack-plugin@5.3.10(@swc/core@1.6.3)(webpack@5.92.0(@swc/core@1.6.3)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.0 - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) + optionalDependencies: + '@swc/core': 1.6.3 terser@5.3.8: dependencies: @@ -17575,7 +19121,7 @@ snapshots: dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5)) + jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -17590,11 +19136,11 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.24.6) esbuild: 0.21.4 - ts-jest@29.1.4(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)))(typescript@5.4.5): + ts-jest@29.1.5(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.12.13)(ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5)) + jest: 29.7.0(@types/node@20.14.5)(ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -17608,35 +19154,17 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.6) - ts-loader@9.5.1(typescript@5.4.5)(webpack@5.92.0): + ts-loader@9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.6.3)): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.16.1 + enhanced-resolve: 5.17.0 micromatch: 4.0.7 semver: 7.6.2 source-map: 0.7.4 typescript: 5.4.5 - webpack: 5.92.0 - - ts-node@10.9.2(@types/node@20.12.13)(typescript@5.4.5): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.13 - acorn: 8.11.3 - acorn-walk: 8.3.2 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.4.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 + webpack: 5.92.0(@swc/core@1.6.3) - ts-node@10.9.2(@types/node@20.14.5)(typescript@5.4.5): + ts-node@10.9.2(@swc/core@1.6.3)(@types/node@20.14.5)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -17653,6 +19181,8 @@ snapshots: typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.6.3 tsconfig-paths@3.15.0: dependencies: @@ -17803,6 +19333,21 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 + + use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 + username@5.1.0: dependencies: execa: 1.0.0 @@ -17862,6 +19407,12 @@ snapshots: - supports-color - terser + vite-plugin-singlefile@2.0.2(rollup@4.18.0)(vite@5.2.12(@types/node@20.14.5)(terser@5.31.0)): + dependencies: + micromatch: 4.0.7 + rollup: 4.18.0 + vite: 5.2.12(@types/node@20.14.5)(terser@5.31.0) + vite@5.2.12(@types/node@20.14.5)(terser@5.31.0): dependencies: esbuild: 0.20.2 @@ -17937,16 +19488,16 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@5.3.4(webpack@5.92.0): + webpack-dev-middleware@5.3.4(webpack@5.92.0(@swc/core@1.6.3)): dependencies: colorette: 2.0.20 memfs: 3.5.3 mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.2.0 - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) - webpack-dev-server@4.15.2(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10)(webpack@5.92.0): + webpack-dev-server@4.15.2(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10)(webpack@5.92.0(@swc/core@1.6.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -17976,10 +19527,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 5.3.4(webpack@5.92.0) + webpack-dev-middleware: 5.3.4(webpack@5.92.0(@swc/core@1.6.3)) ws: 8.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: - webpack: 5.92.0 + webpack: 5.92.0(@swc/core@1.6.3) transitivePeerDependencies: - bufferutil - debug @@ -17994,7 +19545,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.92.0: + webpack@5.92.0(@swc/core@1.6.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.5 @@ -18017,7 +19568,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.92.0) + terser-webpack-plugin: 5.3.10(@swc/core@1.6.3)(webpack@5.92.0(@swc/core@1.6.3)) watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0b1516531..46df38d54 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - "backend" - "browser-extension" - "cli" + - "cli/html-report" - "desktop" - "frontend" - "gateway"