diff --git a/Cargo.lock b/Cargo.lock index 134f769d2..38da19f67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1149,7 +1149,7 @@ dependencies = [ [[package]] name = "command" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "komodo_client", "shlex", @@ -1489,7 +1489,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "database" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "async-compression", @@ -1759,7 +1759,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "bytes", @@ -1801,7 +1801,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "formatting", @@ -1930,7 +1930,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "mogh_error", ] @@ -2109,7 +2109,7 @@ dependencies = [ [[package]] name = "git" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "command", @@ -2709,7 +2709,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "komodo_client", @@ -2835,7 +2835,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "bcrypt", @@ -2865,7 +2865,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "async_timing_util", @@ -2904,7 +2904,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "arc-swap", @@ -2977,7 +2977,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "arc-swap", @@ -4032,7 +4032,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "encoding", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.1.1" +version = "2.2.0-dev-2" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index c95420039..a380809d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "2.1.1" +version = "2.2.0-dev-2" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index 872976a8d..7e6f8a57c 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -58,7 +58,8 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config" # This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*` ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*" -CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ] +ENTRYPOINT [ "entrypoint.sh" ] +CMD [ "core" ] # Label to prevent Komodo from stopping with StopAllContainers LABEL komodo.skip="true" diff --git a/bin/core/src/auth/mod.rs b/bin/core/src/auth/mod.rs index d0cf72040..f2cfbf4e0 100644 --- a/bin/core/src/auth/mod.rs +++ b/bin/core/src/auth/mod.rs @@ -348,6 +348,7 @@ impl AuthImpl for KomodoAuthImpl { additional_audiences: config .oidc_additional_audiences .clone(), + auto_redirect: config.oidc_auto_redirect, } }); Some(&OIDC_CONFIG) diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 360be6400..f90169fb0 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -235,6 +235,9 @@ pub fn core_config() -> &'static CoreConfig { env.komodo_oidc_additional_audiences, ) .unwrap_or(config.oidc_additional_audiences), + oidc_auto_redirect: env + .komodo_oidc_auto_redirect + .unwrap_or(config.oidc_auto_redirect), google_oauth: NamedOauthConfig { enabled: env .komodo_google_oauth_enabled diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 6558b9958..f8f3f79f1 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -170,6 +170,8 @@ pub struct Env { pub komodo_oidc_additional_audiences: Option>, /// Override `oidc_additional_audiences` from file pub komodo_oidc_additional_audiences_file: Option, + /// Override `oidc_auto_redirect` + pub komodo_oidc_auto_redirect: Option, /// Override `google_oauth.enabled` pub komodo_google_oauth_enabled: Option, @@ -525,6 +527,12 @@ pub struct CoreConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub oidc_additional_audiences: Vec, + /// Automatically redirect unauthenticated users to the OIDC provider + /// instead of showing the login page. + /// Users can bypass the redirect by appending `?disableAutoLogin` to the login URL. + #[serde(default)] + pub oidc_auto_redirect: bool, + // ========= // = Oauth = // ========= @@ -837,6 +845,7 @@ impl Default for CoreConfig { oidc_client_secret: Default::default(), oidc_use_full_email: Default::default(), oidc_additional_audiences: Default::default(), + oidc_auto_redirect: Default::default(), google_oauth: Default::default(), github_oauth: Default::default(), auth_rate_limit_disabled: Default::default(), @@ -932,6 +941,7 @@ impl CoreConfig { .iter() .map(|aud| empty_or_redacted(aud)) .collect(), + oidc_auto_redirect: config.oidc_auto_redirect, google_oauth: NamedOauthConfig { enabled: config.google_oauth.enabled, client_id: empty_or_redacted(&config.google_oauth.client_id), diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 75fc8167b..8b314227d 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -1,6 +1,6 @@ { "name": "komodo_client", - "version": "2.1.1", + "version": "2.2.0", "description": "Komodo client package", "homepage": "https://komo.do", "main": "dist/lib.js", @@ -13,7 +13,7 @@ "build": "tsc" }, "dependencies": { - "mogh_auth_client": "^1.2.1" + "mogh_auth_client": "^1.3.0" }, "devDependencies": { "typescript": "^6.0.2" diff --git a/client/core/ts/yarn.lock b/client/core/ts/yarn.lock index dd0a833b6..c6495ca93 100644 --- a/client/core/ts/yarn.lock +++ b/client/core/ts/yarn.lock @@ -7,10 +7,10 @@ jwt-decode@^4.0.0: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== -mogh_auth_client@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.2.1.tgz#c8b5e9da101dc8da7b30586e5c5463f5f7a95edb" - integrity sha512-8uUjgqagwbMW8BKtTRzfQ4txpw+54hqLszbIvcYf99murVIQu5+NsRZ8/vjP/fOMawgXJXCVsR6O2lamm1BCnQ== +mogh_auth_client@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.3.0.tgz#0effd785ccff7a69ab7c900ccffba5a1d5b42015" + integrity sha512-96vCjtx+hP3sFNpfXjbs/eQfz/c6GBYA+MGOgj/9n2B2xJwtm7UDt/9p8OA2JqPXL/9zml5sH6oXIakiRoFsJg== dependencies: jwt-decode "^4.0.0" diff --git a/config/core.config.toml b/config/core.config.toml index c9fdcca21..5067bc06a 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -267,6 +267,13 @@ oidc_use_full_email = false ## Default: empty oidc_additional_audiences = [] +## Automatically redirect unauthenticated users to the OIDC provider +## instead of showing the login page. +## Users can bypass the redirect by appending `?disableAutoLogin` to the login URL. +## Env: KOMODO_OIDC_AUTO_REDIRECT +## Default: false +oidc_auto_redirect = false + ######### # OAUTH # ######### diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index 5834099b4..53c5baed0 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -167,8 +167,13 @@ fn shell() -> &'static str { String::from("/bin/bash") } else if PathBuf::from("/usr/bin/bash").exists() { String::from("/usr/bin/bash") - } else { + } else if PathBuf::from("/bin/sh").exists() { String::from("/bin/sh") + } else if PathBuf::from("/usr/bin/sh").exists() { + String::from("/usr/bin/sh") + } else { + // try to use sh wherever it is on host by name. + String::from("sh") } }) } diff --git a/ui/package.json b/ui/package.json index b3d2f9578..d81f5d9c4 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,34 +10,36 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@mantine/core": "^8.3.15", - "@mantine/form": "^8.3.15", - "@mantine/hooks": "^8.3.15", - "@mantine/notifications": "^8.3.15", - "@mantine/spotlight": "^8.3.15", + "@mantine/core": "^9.0.0", + "@mantine/form": "^9.0.0", + "@mantine/hooks": "^9.0.0", + "@mantine/notifications": "^9.0.0", + "@mantine/spotlight": "^9.0.0", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.90.21", + "@tanstack/react-query": "^5.96.1", "@tanstack/react-table": "^8.21.3", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ansi-to-html": "^0.7.2", - "jotai": "^2.18.0", + "jotai": "^2.19.0", "jotai-family": "^1.0.1", "jotai-location": "^0.6.2", - "lucide-react": "^1.6.0", + "lucide-react": "^1.7.0", + "mogh_ui": "^0.3.1", + "mogh_auth_client": "^1.3.0", "monaco-editor": "^0.55.1", "monaco-yaml": "^5.4.1", "react": "^19.2.4", "react-dom": "^19.2.4", "react-minimal-pie-chart": "^9.1.2", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.14.0", "react-xtermjs": "^1.0.10", - "recharts": "^3.7.0", - "sanitize-html": "^2.17.1", + "recharts": "^3.8.1", + "sanitize-html": "^2.17.2", "shell-quote": "^1.8.3" }, "devDependencies": { - "@types/node": "^25.3.0", + "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/sanitize-html": "^2.16.0", @@ -47,7 +49,7 @@ "postcss": "^8.5.6", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "sass-embedded": "^1.97.3", + "sass-embedded": "^1.99.0", "typescript": "^6.0.2", "vite": "^8.0.2" }, diff --git a/ui/src/app/index.tsx b/ui/src/app/index.tsx index 51eedf905..4da9a3b2d 100644 --- a/ui/src/app/index.tsx +++ b/ui/src/app/index.tsx @@ -4,7 +4,7 @@ import { Suspense } from "react"; import { Outlet } from "react-router-dom"; import Topbar from "@/app/topbar"; import Sidebar from "@/app/sidebar"; -import LoadingScreen from "@/ui/loading-screen"; +import { LoadingScreen } from "mogh_ui"; import UpdateDetails from "@/components/updates/details"; import AlertDetails from "@/components/alerts/details"; diff --git a/ui/src/app/sidebar.tsx b/ui/src/app/sidebar.tsx index 154254d5a..bfff9c1f6 100644 --- a/ui/src/app/sidebar.tsx +++ b/ui/src/app/sidebar.tsx @@ -1,4 +1,4 @@ -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { usableResourcePath } from "@/lib/utils"; import { SIDEBAR_RESOURCES } from "@/resources"; import { Button, Divider, ScrollArea, Stack, Text } from "@mantine/core"; diff --git a/ui/src/app/topbar/alerts.tsx b/ui/src/app/topbar/alerts.tsx index ff3571c21..e42176120 100644 --- a/ui/src/app/topbar/alerts.tsx +++ b/ui/src/app/topbar/alerts.tsx @@ -1,5 +1,5 @@ import { useRead } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { ActionIcon, Box, Center, Menu } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import AlertList from "@/components/alerts/list"; diff --git a/ui/src/app/topbar/index.tsx b/ui/src/app/topbar/index.tsx index 90b6217a3..4fdf15179 100644 --- a/ui/src/app/topbar/index.tsx +++ b/ui/src/app/topbar/index.tsx @@ -10,7 +10,7 @@ import { Text, } from "@mantine/core"; import { useNavigate } from "react-router-dom"; -import ThemeToggle from "@/ui/theme-toggle"; +import { ThemeToggle } from "mogh_ui"; import UserDropdown from "@/app/topbar/user-dropdown"; import TopbarUpdates from "@/app/topbar/updates"; import OmniSearch from "@/app/topbar/omni-search"; diff --git a/ui/src/app/topbar/keyboard-shortcuts.tsx b/ui/src/app/topbar/keyboard-shortcuts.tsx index 896924b65..0be7dbbfe 100644 --- a/ui/src/app/topbar/keyboard-shortcuts.tsx +++ b/ui/src/app/topbar/keyboard-shortcuts.tsx @@ -1,4 +1,4 @@ -import { useSettingsView, useShiftKeyListener } from "@/lib/hooks"; +import { useSettingsView } from "@/lib/hooks"; import { ActionIcon, Divider, @@ -10,6 +10,7 @@ import { } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { Keyboard } from "lucide-react"; +import { useShiftKeyListener } from "mogh_ui"; import { useNavigate } from "react-router-dom"; export default function KeyboardShortcuts() { diff --git a/ui/src/app/topbar/omni-search/hooks.tsx b/ui/src/app/topbar/omni-search/hooks.tsx index 4462e0d65..4d2801673 100644 --- a/ui/src/app/topbar/omni-search/hooks.tsx +++ b/ui/src/app/topbar/omni-search/hooks.tsx @@ -6,7 +6,7 @@ import { useSettingsView, useUser, } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { terminalLink, usableResourcePath } from "@/lib/utils"; import { RESOURCE_TARGETS, ResourceComponents } from "@/resources"; import { diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index 0dd004cd0..c5b8d9ee7 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -1,8 +1,8 @@ import { ActionIcon, Badge, Button, Group } from "@mantine/core"; import { Spotlight, spotlight } from "@mantine/spotlight"; import { useOmniSearch } from "./hooks"; -import { ICONS } from "@/theme/icons"; -import { useShiftKeyListener } from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import { useShiftKeyListener } from "mogh_ui"; import classes from "./index.module.scss"; export default function OmniSearch({}: {}) { @@ -40,6 +40,7 @@ export default function OmniSearch({}: {}) { query={search} onQueryChange={setSearch} clearQueryOnClose={false} + radius="sm" > } diff --git a/ui/src/app/topbar/updates.tsx b/ui/src/app/topbar/updates.tsx index 3174a6633..d4f7583f3 100644 --- a/ui/src/app/topbar/updates.tsx +++ b/ui/src/app/topbar/updates.tsx @@ -1,5 +1,5 @@ import { useRead, useUser, useUserInvalidate, useWrite } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { ActionIcon, Center, diff --git a/ui/src/app/topbar/user-dropdown.tsx b/ui/src/app/topbar/user-dropdown.tsx index c41364da6..91cacd4f3 100644 --- a/ui/src/app/topbar/user-dropdown.tsx +++ b/ui/src/app/topbar/user-dropdown.tsx @@ -21,7 +21,7 @@ import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { MoghAuth } from "komodo_client"; import { useRead, useUser, useUserInvalidate } from "@/lib/hooks"; -import { hexColorByIntention } from "@/lib/color"; +import { hexColorByIntention } from "mogh_ui"; export default function UserDropdown() { const [_, setRerender] = useState(false); diff --git a/ui/src/app/topbar/websocket-status.tsx b/ui/src/app/topbar/websocket-status.tsx index 5f8f0ce1d..0fbad1546 100644 --- a/ui/src/app/topbar/websocket-status.tsx +++ b/ui/src/app/topbar/websocket-status.tsx @@ -1,8 +1,8 @@ -import { hexColorByIntention } from "@/lib/color"; import { useWebsocketConnected, useWebsocketReconnect } from "@/lib/socket"; import { ActionIcon, Box, HoverCard, Text } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { Circle } from "lucide-react"; +import { hexColorByIntention } from "mogh_ui"; export default function WebsocketStatus() { const connected = useWebsocketConnected(); diff --git a/ui/src/components/alerts/card.tsx b/ui/src/components/alerts/card.tsx index 4ebd8fc62..27b602600 100644 --- a/ui/src/components/alerts/card.tsx +++ b/ui/src/components/alerts/card.tsx @@ -1,12 +1,12 @@ import { BoxProps, Flex, FlexProps, Group, Stack } from "@mantine/core"; import { Types } from "komodo_client"; import { AlertTriangle, Check } from "lucide-react"; -import { fmtDate, fmtUpperCamelcase } from "@/lib/formatting"; -import { ICONS } from "@/theme/icons"; +import { fmtDate, fmtUpperCamelcase } from "mogh_ui"; +import { ICONS } from "@/lib/icons"; import { useAlertDetails } from "./details"; import AlertLevel from "./level"; import ResourceLink from "@/resources/link"; -import { hexColorByIntention } from "@/lib/color"; +import { hexColorByIntention } from "mogh_ui"; export default function AlertCard({ alert, diff --git a/ui/src/components/alerts/details.tsx b/ui/src/components/alerts/details.tsx index a99487ea1..6bd7d506a 100644 --- a/ui/src/components/alerts/details.tsx +++ b/ui/src/components/alerts/details.tsx @@ -1,20 +1,16 @@ -import { - fmtDateWithMinutes, - fmtDuration, - fmtUpperCamelcase, -} from "@/lib/formatting"; +import { fmtDateWithMinutes, fmtDuration, fmtUpperCamelcase } from "mogh_ui"; import { useInvalidate, useRead, useUser, useWrite } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { ActionIcon, Drawer, Group, Stack, Text } from "@mantine/core"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { Clock, Link2 } from "lucide-react"; -import CopyButton from "@/ui/copy-button"; -import { MonacoEditor } from "@/components/monaco"; -import LoadingScreen from "@/ui/loading-screen"; +import { CopyButton } from "mogh_ui"; +import { MonacoEditor } from "mogh_ui"; +import { LoadingScreen } from "mogh_ui"; import { atom, useAtom } from "jotai"; import ResourceLink from "@/resources/link"; import { notifications } from "@mantine/notifications"; -import ConfirmButton from "@/ui/confirm-button"; +import { ConfirmButton } from "mogh_ui"; import { To, useLocation, useNavigate } from "react-router-dom"; const alertDetailsAtom = atom(); diff --git a/ui/src/components/alerts/level.tsx b/ui/src/components/alerts/level.tsx index 57dd4e869..db2751c2c 100644 --- a/ui/src/components/alerts/level.tsx +++ b/ui/src/components/alerts/level.tsx @@ -1,5 +1,5 @@ import { alertLevelIntention } from "@/lib/color"; -import StatusBadge from "@/ui/status-badge"; +import { StatusBadge } from "mogh_ui"; import { Types } from "komodo_client"; export default function AlertLevel({ diff --git a/ui/src/components/alerts/list.tsx b/ui/src/components/alerts/list.tsx index be786c1a2..434b3deb6 100644 --- a/ui/src/components/alerts/list.tsx +++ b/ui/src/components/alerts/list.tsx @@ -2,7 +2,7 @@ import { useRead } from "@/lib/hooks"; import { Types } from "komodo_client"; import AlertCard from "./card"; import { Button, Box, BoxProps, Stack } from "@mantine/core"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { Link } from "react-router-dom"; export interface AlertListProps extends BoxProps { diff --git a/ui/src/components/api-keys/new.tsx b/ui/src/components/api-keys/new.tsx index 424d0181f..29015866b 100644 --- a/ui/src/components/api-keys/new.tsx +++ b/ui/src/components/api-keys/new.tsx @@ -1,6 +1,5 @@ -import { useInvalidate, useManageAuth, useWrite } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import CopyText from "@/ui/copy-text"; +import { useInvalidate, useWrite } from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; import { Button, Group, @@ -13,6 +12,7 @@ import { import { useDisclosure } from "@mantine/hooks"; import { Types } from "komodo_client"; import { useState } from "react"; +import { CopyText, useManageAuth } from "mogh_ui"; const ONE_DAY_MS = 1000 * 60 * 60 * 24; type ExpiresOptions = "90 days" | "180 days" | "1 year" | "Never"; @@ -133,12 +133,20 @@ export default function NewApiKey({ userId }: { userId?: string }) { Key - + Secret - + diff --git a/ui/src/components/api-keys/section.tsx b/ui/src/components/api-keys/section.tsx index df87c2b2c..7c9c76191 100644 --- a/ui/src/components/api-keys/section.tsx +++ b/ui/src/components/api-keys/section.tsx @@ -1,8 +1,8 @@ -import { ICONS } from "@/theme/icons"; -import Section, { SectionProps } from "@/ui/section"; +import { ICONS } from "@/lib/icons"; +import { Section, SectionProps, useManageAuth } from "mogh_ui"; import NewApiKey from "./new"; import ApiKeysTable from "./table"; -import { useInvalidate, useManageAuth, useRead, useWrite } from "@/lib/hooks"; +import { useInvalidate, useRead, useWrite } from "@/lib/hooks"; import { notifications } from "@mantine/notifications"; import { Box } from "@mantine/core"; @@ -47,7 +47,6 @@ export default function ApiKeysSection({ isPending={isPending} title="API Keys" titleFz="h3" - icon={} titleRight={ diff --git a/ui/src/components/api-keys/table.tsx b/ui/src/components/api-keys/table.tsx index e2f66d8ee..7496da4a7 100644 --- a/ui/src/components/api-keys/table.tsx +++ b/ui/src/components/api-keys/table.tsx @@ -1,7 +1,7 @@ -import { ICONS } from "@/theme/icons"; -import ConfirmButton from "@/ui/confirm-button"; -import CopyText from "@/ui/copy-text"; -import { DataTable } from "@/ui/data-table"; +import { ICONS } from "@/lib/icons"; +import { ConfirmButton } from "mogh_ui"; +import { CopyText } from "mogh_ui"; +import { DataTable } from "mogh_ui"; import { Text } from "@mantine/core"; import { Types } from "komodo_client"; diff --git a/ui/src/components/batch-executions.tsx b/ui/src/components/batch-executions.tsx index 217d7eeb8..94a1dceb3 100644 --- a/ui/src/components/batch-executions.tsx +++ b/ui/src/components/batch-executions.tsx @@ -1,8 +1,8 @@ -import { fmtUpperCamelcase } from "@/lib/formatting"; +import { fmtUpperCamelcase } from "mogh_ui"; import { useExecute, useSelectedResources, useWrite } from "@/lib/hooks"; -import { sendCopyNotification, usableResourceExecuteKey } from "@/lib/utils"; +import { usableResourceExecuteKey } from "@/lib/utils"; import { UsableResource } from "@/resources"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { Box, Button, @@ -20,6 +20,7 @@ import { import { Types } from "komodo_client"; import { ChevronDown } from "lucide-react"; import { FC, useState } from "react"; +import { sendCopyNotification } from "mogh_ui"; type Request = Types.ExecuteRequest["type"] | Types.WriteRequest["type"]; diff --git a/ui/src/components/config/account-selector.tsx b/ui/src/components/config/account-selector.tsx index 78f28d9cc..cdf74096f 100644 --- a/ui/src/components/config/account-selector.tsx +++ b/ui/src/components/config/account-selector.tsx @@ -1,5 +1,5 @@ import { useRead } from "@/lib/hooks"; -import { ConfigItem } from "@/ui/config/item"; +import { ConfigItem } from "mogh_ui"; import { Select, SelectProps } from "@mantine/core"; export interface AccountSelectorProps extends Omit { diff --git a/ui/src/components/config/add-extra-arg.tsx b/ui/src/components/config/add-extra-arg.tsx index c5607d779..ece0c23b5 100644 --- a/ui/src/components/config/add-extra-arg.tsx +++ b/ui/src/components/config/add-extra-arg.tsx @@ -1,7 +1,8 @@ -import { useRead, useSearchCombobox } from "@/lib/hooks"; -import { filterBySplit } from "@/lib/utils"; +import { useRead } from "@/lib/hooks"; +import { filterBySplit } from "mogh_ui"; import { Button, Combobox } from "@mantine/core"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; +import { useSearchCombobox } from "mogh_ui"; export interface AddExtraArgProps { type: "Deployment" | "Build" | "Stack" | "StackBuild"; diff --git a/ui/src/components/config/image-registry-config.tsx b/ui/src/components/config/image-registry-config.tsx index 203e13cd3..4451735b2 100644 --- a/ui/src/components/config/image-registry-config.tsx +++ b/ui/src/components/config/image-registry-config.tsx @@ -1,11 +1,11 @@ import { useRead } from "@/lib/hooks"; -import { ConfigItem } from "@/ui/config/item"; +import { ConfigItem } from "mogh_ui"; import { ActionIcon, Badge, Group, Text } from "@mantine/core"; import { Types } from "komodo_client"; import ProviderSelector from "./provider-selector"; import AccountSelector from "./account-selector"; import OrganizationSelector from "./organization-selector"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; export interface ImageRegistryConfig { registry: Types.ImageRegistryConfig | undefined; diff --git a/ui/src/components/config/linked-repo.tsx b/ui/src/components/config/linked-repo.tsx index 7bc74c454..506868eb7 100644 --- a/ui/src/components/config/linked-repo.tsx +++ b/ui/src/components/config/linked-repo.tsx @@ -1,6 +1,6 @@ import ResourceLink from "@/resources/link"; import ResourceSelector from "@/resources/selector"; -import { ConfigItem } from "@/ui/config/item"; +import { ConfigItem } from "mogh_ui"; import { Group } from "@mantine/core"; export interface LinkedRepoProps { diff --git a/ui/src/components/config/provider-selector.tsx b/ui/src/components/config/provider-selector.tsx index 0210fcf60..f35949029 100644 --- a/ui/src/components/config/provider-selector.tsx +++ b/ui/src/components/config/provider-selector.tsx @@ -1,5 +1,5 @@ import { useRead } from "@/lib/hooks"; -import { ConfigItem } from "@/ui/config/item"; +import { ConfigItem } from "mogh_ui"; import { Button, Group, Select, SelectProps, TextInput } from "@mantine/core"; import { useState } from "react"; diff --git a/ui/src/components/config/secret-selector.tsx b/ui/src/components/config/secret-selector.tsx index 4979c8998..d783bbdc8 100644 --- a/ui/src/components/config/secret-selector.tsx +++ b/ui/src/components/config/secret-selector.tsx @@ -1,8 +1,9 @@ -import { useSearchCombobox, useSettingsView } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import { filterBySplit } from "@/lib/utils"; +import { useSettingsView } from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import { filterBySplit } from "mogh_ui"; import { Button, Combobox, ComboboxProps } from "@mantine/core"; import { notifications } from "@mantine/notifications"; +import { useSearchCombobox } from "mogh_ui"; import { useNavigate } from "react-router-dom"; export interface SecretSelectorProps extends ComboboxProps { diff --git a/ui/src/components/config/system-command.tsx b/ui/src/components/config/system-command.tsx index b6ddfe2ee..ef808f3cf 100644 --- a/ui/src/components/config/system-command.tsx +++ b/ui/src/components/config/system-command.tsx @@ -1,6 +1,6 @@ import { Stack, TextInput } from "@mantine/core"; import { Types } from "komodo_client"; -import { MonacoEditor } from "@/components/monaco"; +import { MonacoEditor } from "mogh_ui"; export interface SystemCommandProps { value?: Types.SystemCommand; diff --git a/ui/src/components/confirm-modal-with-disable.tsx b/ui/src/components/confirm-modal-with-disable.tsx index 4c34d2d6c..368b922c5 100644 --- a/ui/src/components/confirm-modal-with-disable.tsx +++ b/ui/src/components/confirm-modal-with-disable.tsx @@ -1,5 +1,5 @@ import { useRead } from "@/lib/hooks"; -import ConfirmModal, { ConfirmModalProps } from "@/ui/confirm-modal"; +import { ConfirmModal, ConfirmModalProps } from "mogh_ui"; export interface ConfirmModalWithDisableProps extends Omit< ConfirmModalProps, diff --git a/ui/src/components/dashboard-summary/index.tsx b/ui/src/components/dashboard-summary/index.tsx index d63224a52..7b8508e74 100644 --- a/ui/src/components/dashboard-summary/index.tsx +++ b/ui/src/components/dashboard-summary/index.tsx @@ -1,7 +1,7 @@ import { Group, Paper, Stack, Text } from "@mantine/core"; import { PieChart } from "react-minimal-pie-chart"; import { ReactNode, useMemo } from "react"; -import { ColorIntention, hexColorByIntention } from "@/lib/color"; +import { ColorIntention, hexColorByIntention } from "mogh_ui"; import classes from "./index.module.scss"; export type PieChartItem = { diff --git a/ui/src/components/docker/container-ports.tsx b/ui/src/components/docker/container-ports.tsx index f12eb63d5..fd4a7a875 100644 --- a/ui/src/components/docker/container-ports.tsx +++ b/ui/src/components/docker/container-ports.tsx @@ -9,12 +9,11 @@ import { Text, } from "@mantine/core"; import { Types } from "komodo_client"; +import { DividedChildren, hexColorByIntention } from "mogh_ui"; import { EthernetPort } from "lucide-react"; -import { colorByIntention } from "@/lib/color"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { fmtPortMount } from "@/lib/formatting"; import { useServerAddress } from "@/resources/server/hooks"; -import DividedChildren from "@/ui/divided-children"; export interface ContainerPortsProps extends GroupProps { ports: Types.Port[]; @@ -131,7 +130,7 @@ export function ContainerPort({ gap="sm" wrap="nowrap" > - + {displayText} @@ -149,7 +148,7 @@ export function ContainerPort({ > {link ? ( <> - + {link} ) : ( diff --git a/ui/src/components/docker/container-selector.tsx b/ui/src/components/docker/container-selector.tsx index 4ac147178..812e14a50 100644 --- a/ui/src/components/docker/container-selector.tsx +++ b/ui/src/components/docker/container-selector.tsx @@ -10,10 +10,11 @@ import { Text, } from "@mantine/core"; import { Types } from "komodo_client"; -import { useRead, useSearchCombobox } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import { filterBySplit } from "@/lib/utils"; +import { useRead } from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import { filterBySplit } from "mogh_ui"; import { DOCKER_LINK_ICONS } from "@/components/docker/link"; +import { useSearchCombobox } from "mogh_ui"; export interface ContainerSelectorProps extends ComboboxProps { serverId: string; diff --git a/ui/src/components/docker/containers-section.tsx b/ui/src/components/docker/containers-section.tsx index 555934330..f63d084c0 100644 --- a/ui/src/components/docker/containers-section.tsx +++ b/ui/src/components/docker/containers-section.tsx @@ -1,18 +1,18 @@ import { useRead } from "@/lib/hooks"; -import { filterBySplit } from "@/lib/utils"; +import { filterBySplit } from "mogh_ui"; import { Prune } from "@/resources/server/executions"; -import { ICONS } from "@/theme/icons"; -import { DataTable, SortableHeader } from "@/ui/data-table"; -import Section, { SectionProps } from "@/ui/section"; -import ShowHideButton from "@/ui/show-hide-button"; +import { ICONS } from "@/lib/icons"; +import { DataTable, SortableHeader } from "mogh_ui"; +import { Section, SectionProps } from "mogh_ui"; +import { ShowHideButton } from "mogh_ui"; import { Group } from "@mantine/core"; import { Types } from "komodo_client"; import DockerResourceLink from "./link"; -import StatusBadge from "@/ui/status-badge"; +import { StatusBadge } from "mogh_ui"; import { containerStateIntention } from "@/lib/color"; -import DividedChildren from "@/ui/divided-children"; import ContainerPorts from "@/components/docker/container-ports"; -import SearchInput from "@/ui/search-input"; +import { SearchInput } from "mogh_ui"; +import { DividedChildren } from "mogh_ui"; export interface ContainersSectionProps extends SectionProps { serverId: string; diff --git a/ui/src/components/docker/labels-section.tsx b/ui/src/components/docker/labels-section.tsx index 2a72354d2..2800c5c13 100644 --- a/ui/src/components/docker/labels-section.tsx +++ b/ui/src/components/docker/labels-section.tsx @@ -1,10 +1,10 @@ -import { filterMultitermBySplit } from "@/lib/utils"; -import { ICONS } from "@/theme/icons"; -import Section, { SectionProps } from "@/ui/section"; +import { filterMultitermBySplit } from "mogh_ui"; +import { ICONS } from "@/lib/icons"; +import { Section, SectionProps } from "mogh_ui"; import { Box, GroupProps } from "@mantine/core"; import { useMemo, useState } from "react"; import DockerOptions from "./options"; -import SearchInput from "@/ui/search-input"; +import { SearchInput } from "mogh_ui"; export interface DockerLabelsSectionProps extends Omit< SectionProps, diff --git a/ui/src/components/docker/link.tsx b/ui/src/components/docker/link.tsx index a43752c28..0b118392c 100644 --- a/ui/src/components/docker/link.tsx +++ b/ui/src/components/docker/link.tsx @@ -1,10 +1,11 @@ import { ReactNode } from "react"; import { Types } from "komodo_client"; import { useRead } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import { hexColorByIntention, containerStateIntention } from "@/lib/color"; +import { ICONS } from "@/lib/icons"; +import { containerStateIntention } from "@/lib/color"; import { Box, Group, Text } from "@mantine/core"; import { Link } from "react-router-dom"; +import { hexColorByIntention } from "mogh_ui"; export type DockerResourceType = "Container" | "Network" | "Image" | "Volume"; diff --git a/ui/src/components/export-toml.tsx b/ui/src/components/export-toml.tsx index 2214c0b71..7e622503c 100644 --- a/ui/src/components/export-toml.tsx +++ b/ui/src/components/export-toml.tsx @@ -1,11 +1,11 @@ import { useRead } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { Box, Button, Modal } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { Types } from "komodo_client"; -import { MonacoEditor } from "@/components/monaco"; -import CopyButton from "@/ui/copy-button"; -import LoadingScreen from "@/ui/loading-screen"; +import { MonacoEditor } from "mogh_ui"; +import { CopyButton } from "mogh_ui"; +import { LoadingScreen } from "mogh_ui"; export interface ExportTomlProps { targets?: Types.ResourceTarget[]; @@ -68,6 +68,8 @@ function ExportTomlInner({ ? [allData, allPending] : [resourcesData, resourcesPending]; + const enableFancyToml = useRead("GetCoreInfo", {}).data?.enable_fancy_toml; + return ( {loading && } - + diff --git a/ui/src/components/file-source.tsx b/ui/src/components/file-source.tsx index 3fb24b0bc..a8538cd61 100644 --- a/ui/src/components/file-source.tsx +++ b/ui/src/components/file-source.tsx @@ -1,4 +1,4 @@ -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import { Center, Group, Loader, Text } from "@mantine/core"; import RepoLink from "./repo-link"; import { NotepadText } from "lucide-react"; diff --git a/ui/src/components/inspect-section.tsx b/ui/src/components/inspect-section.tsx index 2ec2885d4..141ce93ba 100644 --- a/ui/src/components/inspect-section.tsx +++ b/ui/src/components/inspect-section.tsx @@ -1,11 +1,11 @@ -import { ICONS } from "@/theme/icons"; -import Section, { SectionProps } from "@/ui/section"; +import { ICONS } from "@/lib/icons"; +import { Section, SectionProps } from "mogh_ui"; import { useState } from "react"; -import { MonacoEditor } from "./monaco"; -import ShowHideButton from "@/ui/show-hide-button"; +import { ShowHideButton } from "mogh_ui"; import { Box } from "@mantine/core"; import { Types } from "komodo_client"; import { useRead } from "@/lib/hooks"; +import { MonacoEditor } from "mogh_ui"; export interface InspectSectionProps extends Omit { /* Inspect a read response */ diff --git a/ui/src/components/log-section.tsx b/ui/src/components/log-section.tsx index 07943e6ac..6b6a8e999 100644 --- a/ui/src/components/log-section.tsx +++ b/ui/src/components/log-section.tsx @@ -1,7 +1,7 @@ import { useRead } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import LogViewer from "@/ui/log-viewer"; -import Section, { SectionProps } from "@/ui/section"; +import { ICONS } from "@/lib/icons"; +import LogViewer from "@/components/log-viewer"; +import { Section, SectionProps } from "mogh_ui"; import { ActionIcon, Button, diff --git a/ui/src/ui/log-viewer.tsx b/ui/src/components/log-viewer.tsx similarity index 100% rename from ui/src/ui/log-viewer.tsx rename to ui/src/components/log-viewer.tsx diff --git a/ui/src/components/maintenance-windows.tsx b/ui/src/components/maintenance-windows.tsx index 79b7834e3..0228ba0c9 100644 --- a/ui/src/components/maintenance-windows.tsx +++ b/ui/src/components/maintenance-windows.tsx @@ -12,10 +12,10 @@ import { } from "@mantine/core"; import { Types } from "komodo_client"; import { useState } from "react"; -import { DataTable, SortableHeader } from "@/ui/data-table"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Calendar, CalendarDays, Clock } from "lucide-react"; import { fmtMaintenanceWindowTime } from "@/lib/formatting"; -import { ICONS } from "@/theme/icons"; +import { ICONS } from "@/lib/icons"; import TimezoneSelector from "./timezone-selector"; export interface ConfigMaintenanceWindowsProps { @@ -319,7 +319,7 @@ function MaintenanceWindowForm({ {formData.schedule_type === "Weekly" && ( } - value={null} - data={groups - .filter(([_, groupArgs]) => groupArgs) - .map(([group, groupArgs]) => ({ - group, - items: (groupArgs as ConfigGroupArgs[]).map((arg) => ({ - label: arg.label, - value: group + arg.label, - })), - }))} - onChange={(group) => { - if (!group) return; - window.location.hash = group; - }} - /> - - {/** CONTENT */} - - {GroupsComponent} - {SaveOrResetComponent} - - - )} - - ); -} diff --git a/ui/src/ui/config/item.tsx b/ui/src/ui/config/item.tsx deleted file mode 100644 index fa681ac15..000000000 --- a/ui/src/ui/config/item.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { fmtSnakeCaseToUpperSpaceCase } from "@/lib/formatting"; -import { ICONS } from "@/theme/icons"; -import EnableSwitch from "@/ui/enable-switch"; -import InputList, { InputListProps } from "@/ui/input-list"; -import { - Button, - createPolymorphicComponent, - Group, - Stack, - StackProps, - SwitchProps, - Text, - TextInput, - TextInputProps, -} from "@mantine/core"; -import { forwardRef, ReactNode } from "react"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -export interface ConfigItemProps extends StackProps { - label?: ReactNode; - labelExtra?: ReactNode; - description?: ReactNode; - children?: ReactNode; -} - -export const ConfigItem = createPolymorphicComponent<"div", ConfigItemProps>( - forwardRef( - ({ label, labelExtra, description, children, ...props }, ref) => { - const labelDescription = (label || description) && ( - - {typeof label === "string" && ( - {fmtSnakeCaseToUpperSpaceCase(label)} - )} - {label && typeof label !== "string" && label} - {description && {description}} - - ); - return ( - - {labelExtra ? ( - - {labelDescription} - {labelExtra} - - ) : ( - labelDescription - )} - {children} - - ); - }, - ), -); - -export function ConfigInput({ - value, - disabled, - placeholder, - onChange, - onValueChange, - onBlur, - inputLeft, - inputRight, - inputProps, - email, - ...itemProps -}: { - value: string | number | undefined; - disabled?: boolean; - placeholder?: string; - onValueChange?: (value: string) => void; - onBlur?: (value: string) => void; - inputLeft?: ReactNode; - inputRight?: ReactNode; - inputProps?: TextInputProps; - email?: boolean; -} & Omit) { - const inputNode = ( - { - onChange?.(e); - onValueChange?.(e.target.value); - }} - onBlur={(e) => onBlur?.(e.target.value)} - {...inputProps} - /> - ); - return ( - - {inputLeft || inputRight ? ( - - {inputLeft} - {inputNode} - {inputRight} - - ) : ( - inputNode - )} - - ); -} - -export function ConfigSwitch({ - value, - disabled, - onCheckedChange, - switchProps, - ...itemProps -}: { - value: boolean | undefined; - disabled: boolean; - onCheckedChange: (value: boolean) => void; - switchProps?: SwitchProps; -} & Omit) { - return ( - - - - ); -} - -export function ConfigList({ - addLabel, - label, - description, - ...inputListProps -}: { label?: string; addLabel?: string } & InputListProps & - Omit) { - return ( - - - {!inputListProps.disabled && ( - - )} - - ); -} diff --git a/ui/src/ui/config/layout.tsx b/ui/src/ui/config/layout.tsx deleted file mode 100644 index 325b850e4..000000000 --- a/ui/src/ui/config/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ReactNode } from "react"; -import Section, { SectionProps } from "@/ui/section"; -import { ICONS } from "@/theme/icons"; - -/** Includes save buttons */ -export default function ConfigLayout({ - title, - icon, - titleOther, - SaveOrReset, - ...sectionProps -}: { - SaveOrReset: ReactNode | undefined; -} & SectionProps) { - const titleProps = titleOther - ? { titleOther } - : { - title: title ?? "Config", - icon: icon ?? , - }; - return
; -} diff --git a/ui/src/ui/config/unsaved-changes.tsx b/ui/src/ui/config/unsaved-changes.tsx deleted file mode 100644 index 437813266..000000000 --- a/ui/src/ui/config/unsaved-changes.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { Group } from "@mantine/core"; - -export default function UnsavedChanges({ fullWidth }: { fullWidth?: boolean }) { - return ( - - - Unsaved changes - - - ); -} diff --git a/ui/src/ui/confirm-button.tsx b/ui/src/ui/confirm-button.tsx deleted file mode 100644 index 4bcf66586..000000000 --- a/ui/src/ui/confirm-button.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { - Button, - ButtonProps, - createPolymorphicComponent, - Loader, -} from "@mantine/core"; -import { Check } from "lucide-react"; -import { - FocusEventHandler, - forwardRef, - MouseEventHandler, - ReactNode, - useEffect, - useState, -} from "react"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -export interface ConfirmButtonProps extends ButtonProps { - icon?: ReactNode; - onClick?: MouseEventHandler; - onBlur?: FocusEventHandler; - /** Passed when button is in confirm mode */ - confirmProps?: ButtonProps; -} - -const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>( - forwardRef( - ( - { - icon, - rightSection, - children, - onClick, - onBlur, - miw, - loading, - disabled, - confirmProps, - ...props - }, - ref, - ) => { - const [clickedOnce, setClickedOnce] = useState(false); - useEffect(() => { - if (clickedOnce) { - const timeout = setTimeout(() => { - setClickedOnce(false); - }, 4_000); - return () => clearTimeout(timeout); - } - }, [clickedOnce]); - return ( - - ); - }, - ), -); - -export default ConfirmButton; diff --git a/ui/src/ui/confirm-icon.tsx b/ui/src/ui/confirm-icon.tsx deleted file mode 100644 index 71edcce79..000000000 --- a/ui/src/ui/confirm-icon.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { - ActionIcon, - ActionIconProps, - createPolymorphicComponent, - Loader, -} from "@mantine/core"; -import { Check } from "lucide-react"; -import { - FocusEventHandler, - forwardRef, - MouseEventHandler, - useEffect, - useState, -} from "react"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -export interface ConfirmIconProps extends ActionIconProps { - onClick?: MouseEventHandler; - onBlur?: FocusEventHandler; -} - -const ConfirmIcon = createPolymorphicComponent<"button", ConfirmIconProps>( - forwardRef( - ({ children, onClick, onBlur, miw, loading, disabled, ...props }, ref) => { - const [clickedOnce, setClickedOnce] = useState(false); - useEffect(() => { - if (clickedOnce) { - const timeout = setTimeout(() => { - setClickedOnce(false); - }, 4_000); - return () => clearTimeout(timeout); - } - }, [clickedOnce]); - return ( - { - e.stopPropagation(); - if (clickedOnce) { - onClick?.(e); - setClickedOnce(false); - } else { - setClickedOnce(true); - } - }} - onBlur={(e) => { - setClickedOnce(false); - onBlur?.(e); - }} - onPointerDown={(e) => e.stopPropagation()} - disabled={disabled || loading} - {...props} - ref={ref} - > - {clickedOnce ? ( - - ) : loading ? ( - - ) : ( - (children ?? ) - )} - - ); - }, - ), -); - -export default ConfirmIcon; diff --git a/ui/src/ui/confirm-modal.tsx b/ui/src/ui/confirm-modal.tsx deleted file mode 100644 index 6f71389e5..000000000 --- a/ui/src/ui/confirm-modal.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - Button, - ButtonProps, - Group, - Loader, - Modal, - ModalProps, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { ReactNode, useState } from "react"; -import ConfirmButton from "./confirm-button"; -import { sendCopyNotification } from "@/lib/utils"; - -export interface ConfirmModalProps extends Omit< - Omit, "onClose">, - "onClick" -> { - children?: ReactNode; - icon?: ReactNode; - disabled?: boolean; - /** User must enter this text to confirm */ - confirmText: string; - title?: ReactNode; - confirmButtonContent?: ReactNode; - onConfirm?: () => Promise; - loading?: boolean; - additional?: ReactNode; - topAdditonal?: ReactNode; - targetProps?: ButtonProps; - targetNoIcon?: boolean; - confirmProps?: ButtonProps; - /** Converts into ConfirmButton */ - disableModal?: boolean; -} - -export default function ConfirmModal({ - children, - icon, - disabled, - confirmText, - title, - confirmButtonContent, - onConfirm, - loading, - additional, - topAdditonal, - targetProps, - targetNoIcon, - confirmProps, - disableModal, - ...modalProps -}: ConfirmModalProps) { - const [opened, { open, close }] = useDisclosure(); - const [input, setInput] = useState(""); - - if (disableModal) { - return ( - - {children} - - ); - } - - return ( - <> - - {title ?? ( - <> - Confirm {children} - - )} - - } - styles={{ content: { padding: "0.5rem" } }} - size="lg" - onClick={(e) => e.stopPropagation()} - {...modalProps} - > - - {topAdditonal} - - { - navigator.clipboard.writeText(confirmText); - sendCopyNotification(); - }} - style={{ cursor: "pointer" }} - > - Please enter {confirmText} below to confirm this action. - {(location.origin.startsWith("https") || - // For dev - location.origin.startsWith("http://localhost:")) && ( - - You may click the text in bold to copy it - - )} - - - setInput(e.target.value)} - error={input === confirmText ? undefined : "Does not match"} - /> - - {additional} - - - - - - - - - - ); -} diff --git a/ui/src/ui/copy-button.tsx b/ui/src/ui/copy-button.tsx deleted file mode 100644 index 0c3b6f014..000000000 --- a/ui/src/ui/copy-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { sendCopyNotification } from "@/lib/utils"; -import { - ActionIcon, - ActionIconProps, - CopyButton as MantineCopyButton, -} from "@mantine/core"; -import { Check, Copy } from "lucide-react"; -import { ReactNode } from "react"; - -export interface CopyButtonProps { - content: string; - icon?: ReactNode; - label?: string; - size?: string | number; - buttonSize?: ActionIconProps["size"]; -} - -export default function CopyButton({ - content, - icon, - label = "content", - size = "1.1rem", - buttonSize = "lg", -}: CopyButtonProps) { - return ( - - {({ copied, copy }) => ( - { - e.stopPropagation(); - copy(); - sendCopyNotification(label); - }} - size={buttonSize} - > - {copied ? : (icon ?? )} - - )} - - ); -} diff --git a/ui/src/ui/copy-text.tsx b/ui/src/ui/copy-text.tsx deleted file mode 100644 index c2b630956..000000000 --- a/ui/src/ui/copy-text.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Group, GroupProps, Text, TextProps } from "@mantine/core"; -import CopyButton from "@/ui/copy-button"; -import { sendCopyNotification } from "@/lib/utils"; - -export interface CopyTextProps extends TextProps { - content: string; - label?: string; - groupProps?: GroupProps; -} - -export default function CopyText({ - content, - label, - groupProps, - ...textProps -}: CopyTextProps) { - return ( - - { - navigator.clipboard.writeText(content); - sendCopyNotification(label); - }} - {...textProps} - > - {content} - - - - ); -} diff --git a/ui/src/ui/create-modal.tsx b/ui/src/ui/create-modal.tsx deleted file mode 100644 index f9e244e5f..000000000 --- a/ui/src/ui/create-modal.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useShiftKeyListener } from "@/lib/hooks"; -import { ICONS } from "@/theme/icons"; -import { - Button, - ButtonProps, - Group, - Modal, - ModalBaseProps, - Stack, - Text, -} from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { ReactNode, useEffect } from "react"; - -export interface CreateModalProps extends ButtonProps { - entityType: string; - configSection: (close: () => void) => ReactNode; - loading?: boolean; - onConfirm: () => Promise; - onOpenChange?: (opened: boolean) => void; - configureLabel?: string; - openShiftKeyListener?: string; - children?: ReactNode; - modalSize?: ModalBaseProps["size"]; -} - -export default function CreateModal({ - entityType, - configSection, - disabled, - loading, - onConfirm, - onOpenChange, - configureLabel = "a unique name", - openShiftKeyListener, - leftSection, - children, - modalSize = "md", - ...targetProps -}: CreateModalProps) { - const [opened, { open, close }] = useDisclosure(); - useEffect(() => onOpenChange?.(opened), [opened]); - useShiftKeyListener( - openShiftKeyListener ?? "___", - () => openShiftKeyListener && !opened && open(), - ); - return ( - <> - - - - Enter {configureLabel} for the new {entityType}. - - - {configSection(close)} - - - - - - - - - - ); -} diff --git a/ui/src/ui/data-table.tsx b/ui/src/ui/data-table.tsx deleted file mode 100644 index b93e59fa5..000000000 --- a/ui/src/ui/data-table.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { - Dispatch, - ReactNode, - SetStateAction, - useEffect, - useState, -} from "react"; -import { - Column, - ColumnDef, - flexRender, - getCoreRowModel, - getSortedRowModel, - Row, - RowSelectionState, - SortingState, - useReactTable, -} from "@tanstack/react-table"; -import { - Box, - BoxProps, - Center, - Checkbox, - Group, - HoverCard, - Loader, - Table, - TableProps, - Text, - UnstyledButton, -} from "@mantine/core"; -import { ArrowDown, ArrowUp, Info, Minus } from "lucide-react"; -import { hexColorByIntention } from "@/lib/color"; - -export interface DataTableProps extends BoxProps { - /** Unique key given to table so sorting can be remembered on local storage */ - tableKey: string; - columns: (ColumnDef | false | undefined)[]; - data: TData[]; - loading?: boolean; - onRowClick?: (row: TData) => void; - noResults?: ReactNode; - defaultSort?: SortingState; - sortDescFirst?: boolean; - selectOptions?: { - selectKey: (row: TData) => string; - onSelect?: (selected: string[]) => void; - state?: [RowSelectionState, Dispatch>]; - disableRow?: boolean | ((row: Row) => boolean); - }; - caption?: string; - tableProps?: TableProps; - noBox?: boolean; - noBorder?: boolean; -} - -export function DataTable({ - tableKey, - columns, - data, - loading, - onRowClick, - noResults = No results, - sortDescFirst = false, - defaultSort = [], - selectOptions, - caption, - tableProps, - noBox, - noBorder, - mah = "max(150px, calc(100vh - 320px))", - ...boxProps -}: DataTableProps) { - const [sorting, setSorting] = useState(defaultSort); - - // intentionally not initialized to clear selected values on table mount - // could add some prop for adding default selected state to preserve between mounts - const _internalState = useState({}); - const [rowSelection, setRowSelection] = selectOptions?.state - ? selectOptions.state - : _internalState; - - const table = useReactTable({ - data, - columns: columns.filter((c) => c) as any, - getCoreRowModel: getCoreRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - state: { - sorting, - rowSelection, - }, - sortDescFirst, - onRowSelectionChange: setRowSelection, - getRowId: selectOptions?.selectKey, - enableRowSelection: selectOptions?.disableRow, - }); - - useEffect(() => { - const stored = localStorage.getItem("data-table-" + tableKey); - const sorting = stored ? (JSON.parse(stored) as SortingState) : null; - if (sorting) setSorting(sorting); - }, [tableKey]); - - useEffect(() => { - localStorage.setItem("data-table-" + tableKey, JSON.stringify(sorting)); - }, [tableKey, sorting]); - - useEffect(() => { - selectOptions?.onSelect?.(Object.keys(rowSelection)); - }, [rowSelection]); - - const rows = table.getPrePaginationRowModel().rows; - - const tableNode = ( - - {caption ? {caption} : null} - - - {table.getHeaderGroups().map((hg, i) => ( - - {i === 0 && selectOptions && ( - - selectOptions.disableRow !== true && - table.toggleAllRowsSelected() - } - style={{ - cursor: "pointer", - borderColor: "var(--mantine-color-accent-border-0)", - borderWidth: 0, - borderRightWidth: 1, - borderStyle: "solid", - }} - > - - - )} - {hg.headers.map((header, i) => { - // const canSort = header.column.getCanSort(); - // const sortState = header.column.getIsSorted(); - return ( - - {header.isPlaceholder ? null : ( - - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - )} - - ); - })} - - ))} - - - - {loading ? ( - - - - - - - - ) : rows.length === 0 ? ( - - - - {noResults} - - - - ) : ( - rows.map((row) => ( - - {selectOptions && ( - row.toggleSelected()}> - - - )} - {row.getVisibleCells().map((cell) => ( - onRowClick(row.original) : undefined - } - style={{ flexWrap: "nowrap", textWrap: "nowrap" }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - )} - -
- ); - - if (noBox) { - return tableNode; - } else { - return ( - - {tableNode} - - ); - } -} - -export const SortableHeader = ({ - column, - title, - description, - sortDescFirst, -}: { - column: Column; - title: string; - description?: ReactNode; - sortDescFirst?: boolean; -}) => { - return ( - - - - - {title} - - {description && ( - - - - - - {description} - - - )} - -
- -
-
-
- ); -}; - -function SortIcon({ - state, - sortDescFirst, -}: { - state: false | "asc" | "desc"; - sortDescFirst?: boolean; -}) { - if (state === "asc") - return sortDescFirst ? : ; - if (state === "desc") - return sortDescFirst ? : ; - return ; -} diff --git a/ui/src/ui/divided-children/index.module.scss b/ui/src/ui/divided-children/index.module.scss deleted file mode 100644 index a3741a763..000000000 --- a/ui/src/ui/divided-children/index.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.divided-children > *:not(:last-child) { - padding-right: var(--mantine-spacing-md); - border-right: 1px solid var(--mantine-color-accent-border-0); -} \ No newline at end of file diff --git a/ui/src/ui/divided-children/index.tsx b/ui/src/ui/divided-children/index.tsx deleted file mode 100644 index e9ee69c88..000000000 --- a/ui/src/ui/divided-children/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createPolymorphicComponent, Group, GroupProps } from "@mantine/core"; -import { forwardRef } from "react"; -import classes from "./index.module.scss"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -const DividedChildren = createPolymorphicComponent<"div", GroupProps>( - forwardRef(({ className, ...props }, ref) => ( - - )), -); - -export default DividedChildren; diff --git a/ui/src/ui/enable-switch.tsx b/ui/src/ui/enable-switch.tsx deleted file mode 100644 index 5caac7a8a..000000000 --- a/ui/src/ui/enable-switch.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Badge, Group, GroupProps, Switch, SwitchProps } from "@mantine/core"; - -export interface EnableSwitchProps extends SwitchProps { - checked?: boolean; - onCheckedChange?: (checked: boolean) => void; - redDisabled?: boolean; - labelProps?: GroupProps; -} - -export default function EnableSwitch({ - checked, - color = "green.9", - label, - onChange, - onCheckedChange, - disabled, - redDisabled = true, - labelProps, - ...props -}: EnableSwitchProps) { - return ( - - {label} - - {checked ? "Enabled" : "Disabled"} - - - } - onChange={(e) => { - onChange?.(e); - onCheckedChange?.(e.target.checked); - }} - {...props} - /> - ); -} diff --git a/ui/src/ui/entity-header.tsx b/ui/src/ui/entity-header.tsx deleted file mode 100644 index 62c396720..000000000 --- a/ui/src/ui/entity-header.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ColorIntention, hexColorByIntention } from "@/lib/color"; -import { ICONS } from "@/theme/icons"; -import { ActionIcon, Group, Stack, Text, TextInput } from "@mantine/core"; -import { FC, ReactNode, useEffect, useState } from "react"; - -export interface EntityHeaderProps { - name?: string; - icon: FC<{ size?: string | number; color?: string }>; - intent: ColorIntention; - state?: ReactNode; - status?: ReactNode; - action?: ReactNode; - onRename?: (name: string) => Promise; - renamePending?: boolean; -} - -export default function EntityHeader({ - name, - icon: Icon, - intent, - state, - status, - action, - onRename: _onRename, - renamePending, -}: EntityHeaderProps) { - const [editingName, setEditingName] = useState(false); - const [newName, setNewName] = useState(name); - useEffect(() => { - setNewName(name); - }, [name]); - const color = hexColorByIntention(intent); - const background = color ? color + "25" : undefined; - const onRename = - _onRename && - (() => { - if (!newName) return; - if (name === newName) { - setEditingName(false); - } else { - _onRename(newName).then(() => setEditingName(false)); - } - }); - return ( - - - - - {name && !onRename && ( - - {name} - - )} - {onRename && ( - setEditingName(true)} - style={{ cursor: editingName ? undefined : "pointer" }} - > - {!editingName && ( - <> - - {name} - - - - - - )} - {editingName && ( - <> - setNewName(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onRename()} - disabled={renamePending} - size="lg" - autoFocus - /> - - - - { - setNewName(name); - setEditingName(false); - }} - > - - - - )} - - )} - - - {state} - - {status} - - - - {action} - - ); -} diff --git a/ui/src/ui/entity-page.tsx b/ui/src/ui/entity-page.tsx deleted file mode 100644 index 74f903782..000000000 --- a/ui/src/ui/entity-page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Group, Stack, StackProps } from "@mantine/core"; -import BackButton from "./back-button"; -import { ReactNode } from "react"; - -export interface EntityPageProps extends StackProps { - backTo?: string; - actions?: ReactNode; -} - -export default function EntityPage({ - backTo, - actions, - children, - ...props -}: EntityPageProps) { - return ( - - - - {actions && {actions}} - - {children} - - ); -} diff --git a/ui/src/ui/fancy-card/index.module.css b/ui/src/ui/fancy-card/index.module.css deleted file mode 100644 index e208a4e97..000000000 --- a/ui/src/ui/fancy-card/index.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.fancy-card { - cursor: pointer; - border: 1px solid color-mix(in srgb, var(--mantine-color-accent-border-1) 40%, transparent); - box-shadow: var(--mantine-shadow-xs); - transition: all 150ms ease; -} - -.fancy-card:hover { - border-color: color-mix(in srgb, var(--mantine-color-accent-border-4) 40%, transparent); - background-color: var(--mantine-color-accent-0); - transform: translateY(-4%); - box-shadow: var(--mantine-shadow-md); -} \ No newline at end of file diff --git a/ui/src/ui/fancy-card/index.tsx b/ui/src/ui/fancy-card/index.tsx deleted file mode 100644 index 8c1e4d7fc..000000000 --- a/ui/src/ui/fancy-card/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { forwardRef } from "react"; -import { Box, BoxProps, createPolymorphicComponent } from "@mantine/core"; -import classes from "./index.module.css"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -interface FancyCardProps extends BoxProps {} - -const FancyCard = createPolymorphicComponent<"div", FancyCardProps>( - forwardRef(({ className, ...props }, ref) => ( - - )), -); - -export default FancyCard; diff --git a/ui/src/ui/hover-error.tsx b/ui/src/ui/hover-error.tsx deleted file mode 100644 index 1b7864f2c..000000000 --- a/ui/src/ui/hover-error.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { Button, Code, Group, HoverCard, Stack, Text } from "@mantine/core"; - -export interface HoverErrorProps { - error: string; - trace: string[]; -} - -export default function HoverError({ error, trace }: HoverErrorProps) { - return ( - - - - - - - - - ERROR: - - {error} - - {trace.length > 0 && ( - - - TRACE: - - {trace.map((error, i) => ( - - - {i + 1}: - - {error} - - ))} - - )} - - - - ); -} diff --git a/ui/src/ui/info-card.tsx b/ui/src/ui/info-card.tsx deleted file mode 100644 index 554514ffc..000000000 --- a/ui/src/ui/info-card.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - createPolymorphicComponent, - Group, - Stack, - StackProps, - Text, - TextProps, -} from "@mantine/core"; -import { forwardRef, ReactNode } from "react"; - -// https://mantine.dev/guides/polymorphic/#create-your-own-polymorphic-components - -export interface InfoCardProps extends StackProps { - title: string; - titleProps?: TextProps; - info?: ReactNode; -} - -const InfoCard = createPolymorphicComponent<"div", InfoCardProps>( - forwardRef( - ({ title, info, titleProps, children, ...props }, ref) => { - return ( - - - - {title} - - {info} - - {children} - - ); - }, - ), -); - -export default InfoCard; diff --git a/ui/src/ui/input-list.tsx b/ui/src/ui/input-list.tsx deleted file mode 100644 index 2b00ec669..000000000 --- a/ui/src/ui/input-list.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { ActionIcon, TextInput, TextInputProps } from "@mantine/core"; - -export interface InputListProps { - field: keyof T; - values: string[]; - disabled: boolean; - set: (update: Partial) => void; - placeholder?: string; - inputProps?: TextInputProps; -} - -export default function InputList({ - field, - values, - disabled, - set, - placeholder, - inputProps, -}: InputListProps) { - return ( - <> - {values.map((arg, i) => ( - { - set({ - [field]: values.map((v, index) => - i === index ? e.target.value : v, - ), - } as Partial); - }} - disabled={disabled} - w={{ base: 230, md: 400 }} - rightSection={ - !disabled && ( - - set({ - [field]: [...values.filter((_, idx) => idx !== i)], - } as Partial) - } - > - - - ) - } - placeholder={placeholder} - {...inputProps} - /> - ))} - - ); -} diff --git a/ui/src/ui/labelled-switch.tsx b/ui/src/ui/labelled-switch.tsx deleted file mode 100644 index 10dafaa01..000000000 --- a/ui/src/ui/labelled-switch.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - Group, - GroupProps, - Switch, - SwitchProps, - Text, - TextProps, -} from "@mantine/core"; -import { ReactNode } from "react"; - -export interface LabelledSwitchProps extends SwitchProps { - checked: boolean | undefined; - onCheckedChange: (checked: boolean) => void; - label?: ReactNode; - groupProps?: GroupProps; - labelProps?: TextProps; -} - -export default function LabelledSwitch({ - checked, - onCheckedChange, - label, - groupProps, - labelProps, - ...switchProps -}: LabelledSwitchProps) { - return ( - { - e.preventDefault(); - onCheckedChange(!checked); - }} - className="bordered-light" - px="xs" - py={4} - bdrs="sm" - style={{ cursor: "pointer" }} - justify="space-between" - w={{ base: "100%", xs: "fit-content" }} - {...groupProps} - > - - {label} - - - - ); -} diff --git a/ui/src/ui/labels-group.tsx b/ui/src/ui/labels-group.tsx deleted file mode 100644 index 968b20726..000000000 --- a/ui/src/ui/labels-group.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Badge, Group, GroupProps } from "@mantine/core"; - -export interface LabelsGroupProps extends GroupProps { - labels: [string, string][]; - showEllipsis?: boolean; -} - -export default function LabelsGroup({ - labels, - showEllipsis, - ...groupProps -}: LabelsGroupProps) { - return ( - - {labels.map(([key, val]) => ( - - {key}={val} - - ))} - {showEllipsis && ( - - ... - - )} - - ); -} diff --git a/ui/src/ui/loading-screen.tsx b/ui/src/ui/loading-screen.tsx deleted file mode 100644 index 8df83df66..000000000 --- a/ui/src/ui/loading-screen.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Center, CenterProps, Loader, LoaderProps } from "@mantine/core"; - -export default function LoadingScreen({ - size = "xl", - mt = "30vh", - ...centerProps -}: { - size?: LoaderProps["size"]; -} & CenterProps) { - return ( -
- -
- ); -} diff --git a/ui/src/ui/mobile-friendly-tabs.tsx b/ui/src/ui/mobile-friendly-tabs.tsx deleted file mode 100644 index 4ae85e7eb..000000000 --- a/ui/src/ui/mobile-friendly-tabs.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { ICONS } from "@/theme/icons"; -import { - Box, - Group, - GroupProps, - MantineBreakpoint, - Select, - Stack, - Tabs, - TabsProps, -} from "@mantine/core"; -import { FC, ReactNode } from "react"; - -export interface Tab { - label?: string; - icon?: FC<{ size?: string | number }>; - hidden?: boolean; - disabled?: boolean; - value: string; - content: ReactNode; -} - -export type TabNoContent = Omit; - -export interface MobileFriendlyTabsProps extends MobileFriendlyTabsSelectorProps { - tabs: Tab[]; - tabsProps?: Omit; -} - -export default function MobileFriendlyTabs(props: MobileFriendlyTabsProps) { - return ( - } - tabs={props.tabs} - value={props.value} - {...props.tabsProps} - /> - ); -} - -export interface MobileFriendlyTabsWrapper extends TabsProps { - Selector: ReactNode; - tabs: Tab[]; - value: string; -} - -export function MobileFriendlyTabsWrapper({ - Selector, - tabs, - value, - children, - ...tabsProps -}: MobileFriendlyTabsWrapper) { - return ( - - {Selector} - - - - - ); -} - -export interface MobileFriendlyTabsSelectorProps { - tabs: TabNoContent[]; - actions?: ReactNode; - value: string; - onValueChange: (value: string) => void; - changeAt?: MantineBreakpoint; - fullIconSize?: string | number; - mobileIconSize?: string | number; - tabProps?: GroupProps; -} - -export function MobileFriendlyTabsSelector({ - tabs: _tabs, - actions, - value, - onValueChange, - changeAt: _changeAt, - fullIconSize = "1.1rem", - mobileIconSize = "1rem", - tabProps, -}: MobileFriendlyTabsSelectorProps) { - const tabs = _tabs.filter((t) => !t.hidden); - const changeAt = - _changeAt ?? (tabs.length > 6 ? "lg" : tabs.length > 3 ? "md" : "sm"); - const SelectedIcon = tabs.find((tab) => tab.value === value)?.icon; - return ( - <> - {/* DESKTOP VIEW */} - - - {tabs.map(({ value: tabValue, label, icon: Icon, disabled }) => ( - onValueChange(tabValue)} - w="fit-content" - > - - {Icon && } - {label ?? tabValue} - - - ))} - - {actions} - - - {/* MOBILE VIEW */} - -