From db3749619ad06083c605ff720df37afabfc7adec Mon Sep 17 00:00:00 2001 From: Semahegn Date: Wed, 27 May 2026 01:29:04 +0300 Subject: [PATCH 01/14] test: add monitor test layers and coverage reporting --- .github/workflows/test.yml | 12 ++- .gitignore | 1 + README.md | 6 ++ app/monitors/http.server.test.ts | 125 +++++++++++++++++++++++ app/monitors/monitor.integration.test.ts | 77 ++++++++++++++ app/monitors/tcp.server.test.ts | 45 ++++++++ app/queues/monitor.server.test.ts | 98 ++++++++++++++++++ package.json | 4 + test/saml-idp | 2 +- vitest.config.ts | 5 + 10 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 app/monitors/http.server.test.ts create mode 100644 app/monitors/monitor.integration.test.ts create mode 100644 app/monitors/tcp.server.test.ts create mode 100644 app/queues/monitor.server.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c600a3c2..c8116eb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,7 +108,17 @@ jobs: run: pnpm install - name: โšก Run vitest - run: pnpm exec vitest --coverage + run: pnpm run test:coverage + + - name: ๐Ÿ“Š Coverage summary + run: | + node -e "const summary=require('./coverage/coverage-summary.json').total; const pct=(value)=>typeof value==='number'?value.toFixed(1):value; const lines=pct(summary.lines.pct); const functions=pct(summary.functions.pct); const branches=pct(summary.branches.pct); const statements=pct(summary.statements.pct); const row=`| Metric | Coverage |\n| --- | --- |\n| Lines | ${lines}% |\n| Functions | ${functions}% |\n| Branches | ${branches}% |\n| Statements | ${statements}% |\n`; console.log('## Coverage\\n'); console.log(row);" >> "$GITHUB_STEP_SUMMARY" + + - name: ๐Ÿ“ฆ Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: vitest-coverage + path: coverage # cypress: # name: โšซ๏ธ Cypress diff --git a/.gitignore b/.gitignore index 1cab40f0..bd43c540 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ node_modules /cypress/screenshots /cypress/videos /postgres-data +/coverage diff --git a/README.md b/README.md index b880074a..009e6326 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ This repository uses commitizen. Commit code changes for pr's with `pnpm run com Use `pnpm` for the root project and commit `pnpm-lock.yaml`. Do not regenerate a root `package-lock.json`. +## ๐Ÿงช Testing + +- Run the monitor-focused unit and integration layers with `pnpm run test:monitor` +- Run full Vitest coverage with `pnpm run test:coverage` +- Coverage reports are written to `coverage/` as HTML, LCOV, and JSON summary files, and CI uploads the same directory as an artifact + ## ๐Ÿ† Credits Atlas System was originally created and made open source by the Riverside Healthcare Analytics team. See the [credits](https://www.atlas.bi/about/) for more details. diff --git a/app/monitors/http.server.test.ts b/app/monitors/http.server.test.ts new file mode 100644 index 00000000..e4ab9a93 --- /dev/null +++ b/app/monitors/http.server.test.ts @@ -0,0 +1,125 @@ +// @vitest-environment node + +import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const requestMock = + vi.fn<(args: AxiosRequestConfig) => Promise>>(); +const ntlmClientMock = + vi.fn<() => (args: AxiosRequestConfig) => Promise>>(); + +vi.mock("axios", () => ({ + default: { + request: requestMock, + }, + request: requestMock, +})); + +vi.mock("axios-ntlm", () => ({ + NtlmClient: ntlmClientMock, +})); + +vi.mock("~/notifications/notifier", () => ({ + default: vi.fn(), +})); + +vi.mock("~/models/monitor.server", () => ({ + monitorError: vi.fn(), + updateMonitor: vi.fn(), + setFeedError: vi.fn(), +})); + +vi.mock("@/lib/utils", () => ({ + decrypt: vi.fn((value: string) => `decrypted:${value}`), +})); + +describe("HttpCheck", () => { + beforeEach(() => { + requestMock.mockReset(); + ntlmClientMock.mockReset(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("builds a basic-auth JSON request for axios", async () => { + requestMock.mockResolvedValue({ + status: 200, + data: { ok: true }, + request: { res: { socket: { getPeerCertificate: vi.fn() } } }, + }); + + const { HttpCheck } = await import("./http.server"); + + await HttpCheck({ + httpAuthentication: "basic", + httpUsername: "atlas", + httpPassword: "secret", + httpUrl: "https://atlas.test/health", + httpMethod: "POST", + httpBody: JSON.stringify({ alive: true }), + httpHeaders: JSON.stringify({ "X-Test": "1" }), + httpAcceptedStatusCodes: ["200s"], + }); + + expect(requestMock).toHaveBeenCalledTimes(1); + const options = requestMock.mock.calls[0][0]; + expect(options.url).toBe("https://atlas.test/health"); + expect(options.method).toBe("post"); + expect(options.data).toEqual({ alive: true }); + expect(options.headers).toMatchObject({ + Authorization: + "Basic " + Buffer.from("atlas:decrypted:secret").toString("base64"), + "Content-Type": "application/json", + "X-Test": "1", + }); + expect(options.validateStatus?.(204)).toBe(true); + expect(options.validateStatus?.(500)).toBe(false); + }); + + it("uses the NTLM client when configured", async () => { + const ntlmRequest = vi.fn().mockResolvedValue({ + status: 200, + data: { ok: true }, + request: { res: { socket: { getPeerCertificate: vi.fn() } } }, + }); + ntlmClientMock.mockReturnValue(ntlmRequest); + + const { HttpCheck } = await import("./http.server"); + + await HttpCheck({ + httpAuthentication: "ntlm", + httpUsername: "atlas", + httpPassword: "secret", + httpDomain: "ATLAS", + httpWorkstation: "monitor-box", + httpUrl: "https://atlas.test/ntlm", + httpMethod: "GET", + }); + + expect(ntlmClientMock).toHaveBeenCalledWith({ + username: "atlas", + password: "decrypted:secret", + domain: "ATLAS", + workstation: "monitor-box", + }); + expect(ntlmRequest).toHaveBeenCalledTimes(1); + expect(requestMock).not.toHaveBeenCalled(); + }); + + it("fails for invalid JSON bodies before any request is sent", async () => { + const { HttpCheck } = await import("./http.server"); + + await expect( + HttpCheck({ + httpUrl: "https://atlas.test/health", + httpBodyEncoding: "json", + httpBody: "{not-valid-json}", + }), + ).rejects.toThrow("JSON body is invalid."); + + expect(requestMock).not.toHaveBeenCalled(); + expect(ntlmClientMock).not.toHaveBeenCalled(); + }); +}); diff --git a/app/monitors/monitor.integration.test.ts b/app/monitors/monitor.integration.test.ts new file mode 100644 index 00000000..3ce1c000 --- /dev/null +++ b/app/monitors/monitor.integration.test.ts @@ -0,0 +1,77 @@ +// @vitest-environment node + +import http from "node:http"; +import net from "node:net"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("~/notifications/notifier", () => ({ + default: vi.fn(), +})); + +vi.mock("~/models/monitor.server", () => ({ + monitorError: vi.fn(), + updateMonitor: vi.fn(), + setFeedError: vi.fn(), +})); + +const serversToClose: Array = []; + +afterEach(async () => { + await Promise.all( + serversToClose.splice(0).map( + (server) => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }), + ), + ); +}); + +function listen(server: http.Server | net.Server) { + serversToClose.push(server); + return new Promise((resolve, reject) => { + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("Failed to resolve test server port.")); + return; + } + resolve(address.port); + }); + server.once("error", reject); + }); +} + +describe("monitor transport integrations", () => { + it("checks a live HTTP endpoint", async () => { + const server = http.createServer((_req, res) => { + res.writeHead(204); + res.end(); + }); + const port = await listen(server); + const { HttpCheck } = await import("./http.server"); + + const { res } = await HttpCheck({ + httpUrl: `http://127.0.0.1:${port}/health`, + httpAcceptedStatusCodes: ["200s"], + }); + + expect(res.status).toBe(204); + }); + + it("checks an open TCP port", async () => { + const server = net.createServer((socket) => socket.end()); + const port = await listen(server); + const { TcpCheck } = await import("./tcp.server"); + + const result = await TcpCheck({ + address: "127.0.0.1", + port, + }); + + expect(result.avg).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/app/monitors/tcp.server.test.ts b/app/monitors/tcp.server.test.ts new file mode 100644 index 00000000..b7285c43 --- /dev/null +++ b/app/monitors/tcp.server.test.ts @@ -0,0 +1,45 @@ +// @vitest-environment node + +import { describe, expect, it, vi } from "vitest"; + +const pingMock = vi.fn(); + +vi.mock("tcp-ping", () => ({ + default: { + ping: pingMock, + }, +})); + +vi.mock("~/notifications/notifier", () => ({ + default: vi.fn(), +})); + +vi.mock("~/models/monitor.server", () => ({ + monitorError: vi.fn(), + updateMonitor: vi.fn(), +})); + +describe("TcpCheck", () => { + it("delegates to tcp-ping with a single attempt", async () => { + pingMock.mockImplementation( + ( + _options: { address: string; port: number; attempts: number }, + callback: (error: null, data: { avg: number }) => void, + ) => callback(null, { avg: 12 }), + ); + + const { TcpCheck } = await import("./tcp.server"); + + await expect( + TcpCheck({ + address: "127.0.0.1", + port: 8080, + }), + ).resolves.toEqual({ avg: 12 }); + + expect(pingMock).toHaveBeenCalledWith( + { address: "127.0.0.1", port: 8080, attempts: 1 }, + expect.any(Function), + ); + }); +}); diff --git a/app/queues/monitor.server.test.ts b/app/queues/monitor.server.test.ts new file mode 100644 index 00000000..6b9089d8 --- /dev/null +++ b/app/queues/monitor.server.test.ts @@ -0,0 +1,98 @@ +// @vitest-environment node + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const queueMock = vi.fn( + (_name: string, handler: (job: string) => Promise) => { + return handler; + }, +); +const getMonitorMock = vi.fn(); +const windowsMonitorMock = vi.fn(); +const ubuntuMonitorMock = vi.fn(); +const httpMonitorMock = vi.fn(); +const sqlServerMonitorMock = vi.fn(); +const tcpMonitorMock = vi.fn(); + +vi.mock("quirrel/remix", () => ({ + Queue: queueMock, +})); + +vi.mock("~/models/monitor.server", () => ({ + getMonitor: getMonitorMock, +})); + +vi.mock("~/monitors/windows.server", () => ({ + default: windowsMonitorMock, +})); + +vi.mock("~/monitors/ubuntu.server", () => ({ + default: ubuntuMonitorMock, +})); + +vi.mock("~/monitors/http.server", () => ({ + default: httpMonitorMock, +})); + +vi.mock("~/monitors/sqlServer.server", () => ({ + default: sqlServerMonitorMock, +})); + +vi.mock("~/monitors/tcp.server", () => ({ + default: tcpMonitorMock, +})); + +describe("monitor queue", () => { + beforeEach(() => { + queueMock.mockClear(); + getMonitorMock.mockReset(); + windowsMonitorMock.mockReset(); + ubuntuMonitorMock.mockReset(); + httpMonitorMock.mockReset(); + sqlServerMonitorMock.mockReset(); + tcpMonitorMock.mockReset(); + }); + + it("dispatches http monitors to the HTTP runner", async () => { + getMonitorMock.mockResolvedValue({ id: "1", type: "http" }); + const queueHandler = (await import("./monitor.server")).default; + + await queueHandler("1"); + + expect(queueMock).toHaveBeenCalledWith( + "queues/monitor", + expect.any(Function), + ); + expect(httpMonitorMock).toHaveBeenCalledWith({ + monitor: { id: "1", type: "http" }, + }); + expect(sqlServerMonitorMock).not.toHaveBeenCalled(); + expect(tcpMonitorMock).not.toHaveBeenCalled(); + }); + + it("dispatches sqlServer monitors to the SQL runner", async () => { + getMonitorMock.mockResolvedValue({ id: "2", type: "sqlServer" }); + const queueHandler = (await import("./monitor.server")).default; + + await queueHandler("2"); + + expect(sqlServerMonitorMock).toHaveBeenCalledWith({ + monitor: { id: "2", type: "sqlServer" }, + }); + expect(httpMonitorMock).not.toHaveBeenCalled(); + expect(tcpMonitorMock).not.toHaveBeenCalled(); + }); + + it("returns without dispatch when the monitor is missing", async () => { + getMonitorMock.mockResolvedValue(null); + const queueHandler = (await import("./monitor.server")).default; + + await expect(queueHandler("missing")).resolves.toBeUndefined(); + + expect(windowsMonitorMock).not.toHaveBeenCalled(); + expect(ubuntuMonitorMock).not.toHaveBeenCalled(); + expect(httpMonitorMock).not.toHaveBeenCalled(); + expect(sqlServerMonitorMock).not.toHaveBeenCalled(); + expect(tcpMonitorMock).not.toHaveBeenCalled(); + }); +}); diff --git a/package.json b/package.json index ba5e7658..eab89228 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,10 @@ "start": "cross-env NODE_ENV=production node ./build/server.js", "start:mocks": "cross-env NODE_ENV=test DISABLE_BACKGROUND_SERVICES=1 node --require ./mocks --require dotenv/config ./build/server.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:monitor": "run-s test:monitor:unit test:monitor:integration", + "test:monitor:unit": "vitest run app/monitors/http.server.test.ts app/monitors/tcp.server.test.ts app/queues/monitor.server.test.ts", + "test:monitor:integration": "vitest run app/monitors/monitor.integration.test.ts", "test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"pnpm exec cypress open\"", "pretest:e2e:run": "pnpm run build", "test:e2e:run": "cross-env PORT=8811 start-server-and-test start:mocks http://localhost:8811 \"pnpm exec cypress run\"", diff --git a/test/saml-idp b/test/saml-idp index a0bc7035..5c726484 160000 --- a/test/saml-idp +++ b/test/saml-idp @@ -1 +1 @@ -Subproject commit a0bc70351fc7473996ffae85c7b40844831f0a9a +Subproject commit 5c726484166c1dbf6bf044e5151894458d082bcf diff --git a/vitest.config.ts b/vitest.config.ts index b8b19b3f..fb9927d6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,5 +20,10 @@ module.exports = { ".*\\/build\\/.*", ".*\\/postgres-data\\/.*", ], + coverage: { + provider: "v8", + reporter: ["text", "json-summary", "html", "lcov"], + reportsDirectory: "./coverage", + }, }, }; From f0b91402a9c0c22fbe2a21f18163d7ebb41ace78 Mon Sep 17 00:00:00 2001 From: Semahegn Date: Wed, 27 May 2026 15:14:19 +0300 Subject: [PATCH 02/14] fix: replace the backtick template literal with string concatenation --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8116eb5..fdde660a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,7 +112,7 @@ jobs: - name: ๐Ÿ“Š Coverage summary run: | - node -e "const summary=require('./coverage/coverage-summary.json').total; const pct=(value)=>typeof value==='number'?value.toFixed(1):value; const lines=pct(summary.lines.pct); const functions=pct(summary.functions.pct); const branches=pct(summary.branches.pct); const statements=pct(summary.statements.pct); const row=`| Metric | Coverage |\n| --- | --- |\n| Lines | ${lines}% |\n| Functions | ${functions}% |\n| Branches | ${branches}% |\n| Statements | ${statements}% |\n`; console.log('## Coverage\\n'); console.log(row);" >> "$GITHUB_STEP_SUMMARY" + node -e "const summary=require('./coverage/coverage-summary.json').total; const pct=(value)=>typeof value==='number'?value.toFixed(1):value; const lines=pct(summary.lines.pct); const functions=pct(summary.functions.pct); const branches=pct(summary.branches.pct); const statements=pct(summary.statements.pct); const row='| Metric | Coverage |\n| --- | --- |\n| Lines | '+lines+'% |\n| Functions | '+functions+'% |\n| Branches | '+branches+'% |\n| Statements | '+statements+'% |\n'; console.log('## Coverage\n'); console.log(row);" >> "$GITHUB_STEP_SUMMARY" - name: ๐Ÿ“ฆ Upload coverage artifact uses: actions/upload-artifact@v4 From 8943ec0a5d7a82bff29657e3e93b38821d661895 Mon Sep 17 00:00:00 2001 From: Semahegn Date: Thu, 28 May 2026 18:08:16 +0300 Subject: [PATCH 03/14] chore(tailwind): run official v4 updater --- styles/globals.css | 90 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/styles/globals.css b/styles/globals.css index c3cacdc9..37320f75 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,6 +1,90 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; + +@plugin 'tailwindcss-animate'; + +@custom-variant dark (&:is(.dark *)); + +@utility container { + margin-inline: auto; + padding-inline: 2rem; + @media (width >= --theme(--breakpoint-sm)) { + max-width: none; + } + @media (width >= 1400px) { + max-width: 1400px; + } +} + +@theme { + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} @layer base { :root { From 5c4d9bc86609d6aa208e90b40c88c4a2e4a9ad91 Mon Sep 17 00:00:00 2001 From: Semahegn Date: Thu, 28 May 2026 18:36:32 +0300 Subject: [PATCH 04/14] chore(tailwind): complete v4 updater migration --- app/components/ui/alert.tsx | 2 +- app/components/ui/badge.tsx | 2 +- app/components/ui/button.tsx | 2 +- app/components/ui/checkbox.tsx | 2 +- app/components/ui/command.tsx | 10 +-- app/components/ui/dialog.tsx | 4 +- app/components/ui/dropdown-menu.tsx | 12 +-- app/components/ui/input.tsx | 2 +- app/components/ui/multiselect.tsx | 6 +- app/components/ui/navigation-menu.tsx | 8 +- app/components/ui/popover.tsx | 2 +- app/components/ui/scroll-area.tsx | 4 +- app/components/ui/select.tsx | 10 +-- app/components/ui/separator.tsx | 2 +- app/components/ui/sheet.tsx | 4 +- app/components/ui/switch.tsx | 2 +- app/components/ui/table.tsx | 4 +- app/components/ui/tabs.tsx | 4 +- app/components/ui/textarea.tsx | 2 +- .../_auth.$monitorType/table_columns.tsx | 2 +- .../drive.tsx | 6 +- .../responseTime.tsx | 6 +- .../route.tsx | 2 +- .../sql.tsx | 2 +- .../ssh.tsx | 2 +- .../route.tsx | 4 +- .../route.tsx | 4 +- .../route.tsx | 2 +- .../route.tsx | 4 +- .../route.tsx | 4 +- .../route.tsx | 14 ++-- package.json | 5 +- postcss.config.js | 4 +- tailwind.config.ts | 76 ------------------- 34 files changed, 71 insertions(+), 150 deletions(-) delete mode 100644 tailwind.config.ts diff --git a/app/components/ui/alert.tsx b/app/components/ui/alert.tsx index f10fc26b..980987f6 100644 --- a/app/components/ui/alert.tsx +++ b/app/components/ui/alert.tsx @@ -3,7 +3,7 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + "relative w-full rounded-lg border p-4 has-[svg]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { variants: { variant: { diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx index 24cd1701..b15a86d2 100644 --- a/app/components/ui/badge.tsx +++ b/app/components/ui/badge.tsx @@ -3,7 +3,7 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; 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", + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 71325cde..22a5a785 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -4,7 +4,7 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; const buttonVariants = cva( - "inline-flex items-center justify-center 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", + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { diff --git a/app/components/ui/checkbox.tsx b/app/components/ui/checkbox.tsx index ce47b6f1..8c5f8658 100644 --- a/app/components/ui/checkbox.tsx +++ b/app/components/ui/checkbox.tsx @@ -12,7 +12,7 @@ const Checkbox = React.forwardRef< { return ( - + {children} @@ -41,7 +41,7 @@ const CommandDialogDynamic = ({ children, ...props }: CommandDialogProps) => { {children} @@ -58,7 +58,7 @@ const CommandInput = React.forwardRef< {children} - + Close diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx index c9cfac3a..01b039f5 100644 --- a/app/components/ui/dropdown-menu.tsx +++ b/app/components/ui/dropdown-menu.tsx @@ -26,7 +26,7 @@ const DropdownMenuSubTrigger = React.forwardRef< ( { if (e.key === "Enter") { handleUnselect(item); @@ -119,7 +119,7 @@ export function MultiSelect({ onBlur={() => setOpen(false)} onFocus={() => setOpen(true)} placeholder={placeholder} - className="ml-0 bg-transparent outline-none placeholder:text-muted-foreground flex-1" + className="ml-0 bg-transparent outline-hidden placeholder:text-muted-foreground flex-1" /> @@ -132,7 +132,7 @@ export function MultiSelect({ }} > {open && selectables.length > 0 ? ( -
+
{/*No results found.*/} {selectables.map((framework) => { diff --git a/app/components/ui/navigation-menu.tsx b/app/components/ui/navigation-menu.tsx index 5aae2f13..d62790db 100644 --- a/app/components/ui/navigation-menu.tsx +++ b/app/components/ui/navigation-menu.tsx @@ -40,7 +40,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; const NavigationMenuItem = NavigationMenuPrimitive.Item; const navigationMenuTriggerStyle = cva( - "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50", + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50", ); const NavigationMenuTrigger = React.forwardRef< @@ -54,7 +54,7 @@ const NavigationMenuTrigger = React.forwardRef< > {children}{" "}