mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-02 08:29:18 -06:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15eaeab68d | ||
|
|
89ea5ad5a4 | ||
|
|
c05dde3678 | ||
|
|
a17a1005c7 | ||
|
|
d13314eb98 | ||
|
|
f41a61116a | ||
|
|
13fbcae105 | ||
|
|
71c437962f | ||
|
|
e4147ccdaf | ||
|
|
5089375211 | ||
|
|
e489812af4 | ||
|
|
7373ff8d0e | ||
|
|
4e1cd32f3f | ||
|
|
57211746f1 | ||
|
|
5e153fb02b | ||
|
|
b28a413005 | ||
|
|
691671abbd | ||
|
|
504e81c2f7 |
Generated
+13
-13
@@ -1149,7 +1149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"shlex",
|
||||
@@ -1489,7 +1489,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1759,7 +1759,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1801,7 +1801,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1930,7 +1930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"mogh_error",
|
||||
]
|
||||
@@ -2109,7 +2109,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2709,7 +2709,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2835,7 +2835,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2863,7 +2863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2902,7 +2902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2975,7 +2975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -4030,7 +4030,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"encoding",
|
||||
@@ -5983,7 +5983,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-118"
|
||||
version = "2.0.0-dev-120"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -77,7 +77,7 @@ indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
toml = "0.9.11+spec-1.1.0"
|
||||
toml = "0.9.11"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
serde_json = "1.0.149"
|
||||
serde_qs = "1.0.0"
|
||||
|
||||
@@ -3,14 +3,14 @@ module.exports = {
|
||||
"postcss-preset-mantine": {},
|
||||
"postcss-simple-vars": {
|
||||
variables: {
|
||||
"mantine-breakpoint-xs": "36em",
|
||||
"mantine-breakpoint-sm": "48em",
|
||||
"mantine-breakpoint-md": "62em",
|
||||
"mantine-breakpoint-lg": "75em",
|
||||
"mantine-breakpoint-xl": "88em",
|
||||
"mantine-breakpoint-xl2": "104em",
|
||||
"mantine-breakpoint-xl3": "120em",
|
||||
"mantine-breakpoint-xl4": "136em",
|
||||
"mantine-breakpoint-xs": "40em",
|
||||
"mantine-breakpoint-sm": "54em",
|
||||
"mantine-breakpoint-md": "68em",
|
||||
"mantine-breakpoint-lg": "82em",
|
||||
"mantine-breakpoint-xl": "96em",
|
||||
"mantine-breakpoint-xl2": "110em",
|
||||
"mantine-breakpoint-xl3": "126em",
|
||||
"mantine-breakpoint-xl4": "142em",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/lib/formatting";
|
||||
import { useInvalidate, useRead, useUser, useWrite } from "@/lib/hooks";
|
||||
import { ResourceComponents, UsableResource } from "@/resources";
|
||||
import { Drawer, Group, Stack, Text } from "@mantine/core";
|
||||
import { ActionIcon, Drawer, Group, Stack, Text } from "@mantine/core";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import { Clock, Link2 } from "lucide-react";
|
||||
import CopyButton from "@/ui/copy-button";
|
||||
@@ -92,7 +92,12 @@ export function AlertDetailsContent({
|
||||
return (
|
||||
<Stack gap="xl" m="md">
|
||||
{/** HEADER */}
|
||||
<Text fz="h1">{fmtUpperCamelcase(alert.data.type)}</Text>
|
||||
<Group justify="space-between">
|
||||
<Text fz="h1">{fmtUpperCamelcase(alert.data.type)}</Text>
|
||||
<ActionIcon size="lg" variant="filled" color="red" onClick={close}>
|
||||
<ICONS.Clear size="1.3rem" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/** DETAILS */}
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function ApiKeysSection({
|
||||
isPending={isPending}
|
||||
title="Api Keys"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Key size="1.2rem" />}
|
||||
titleRight={
|
||||
<Box ml="md">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
useMatches,
|
||||
} from "@mantine/core";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
@@ -62,19 +63,18 @@ function BatchExecutionsDropdownMenu<T extends Request>({
|
||||
onSelect: (item: T) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const width = useMatches({
|
||||
base: "target",
|
||||
xs: 250,
|
||||
});
|
||||
return (
|
||||
<Menu
|
||||
position="bottom-start"
|
||||
offset={16}
|
||||
disabled={disabled}
|
||||
width="target"
|
||||
>
|
||||
<Menu position="bottom-start" offset={16} disabled={disabled} width={width}>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
leftSection={<ICONS.Execution size="1rem" />}
|
||||
rightSection={<ChevronDown size="1rem" />}
|
||||
disabled={disabled}
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
w={{ base: "100%", sm: "fit-content" }}
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function ContainersSection({
|
||||
<Section
|
||||
titleOther={titleOther}
|
||||
title={!titleOther ? "Containers" : undefined}
|
||||
|
||||
icon={!titleOther ? <ICONS.Container size="1.3rem" /> : undefined}
|
||||
actions={
|
||||
(pruneButton && !allRunning) || _search || setShow ? (
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function DockerLabelsSection({
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Box>
|
||||
}
|
||||
|
||||
{...props}
|
||||
>
|
||||
<DockerOptions options={filtered} {...groupProps} />
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function BasePermissionsSection({
|
||||
}
|
||||
title="Base Permissions on Resource Types"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
actions={
|
||||
<Group>
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function SpecificPermissionsSection({
|
||||
<Section
|
||||
title="Per Resource Permissions"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
actions={
|
||||
<Group>
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRead } from "@/lib/hooks";
|
||||
import { useWebsocketMessages } from "@/lib/socket";
|
||||
import { updateLogToHtml, versionIsNone } from "@/lib/utils";
|
||||
import { ResourceComponents, UsableResource } from "@/resources";
|
||||
import { Code, Drawer, Group, Stack, Text } from "@mantine/core";
|
||||
import { ActionIcon, Code, Drawer, Group, Stack, Text } from "@mantine/core";
|
||||
import UserAvatar from "@/components/user-avatar";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import { Clock, Link2, SquarePen } from "lucide-react";
|
||||
@@ -62,6 +62,7 @@ export default function UpdateDetails({
|
||||
}
|
||||
|
||||
export function UpdateDetailsContent({ id }: { id: string }) {
|
||||
const { close } = useUpdateDetails();
|
||||
const { data: update, refetch } = useRead("GetUpdate", { id });
|
||||
// Listen for updates on the update id and refetch
|
||||
useWebsocketMessages("update-details", (update) => {
|
||||
@@ -80,10 +81,15 @@ export function UpdateDetailsContent({ id }: { id: string }) {
|
||||
return (
|
||||
<Stack gap="xl" m="md">
|
||||
{/** HEADER */}
|
||||
<Text fz="h2">
|
||||
{fmtOperation(update.operation)}{" "}
|
||||
{!versionIsNone(update.version) && fmtVersion(update.version)}
|
||||
</Text>
|
||||
<Group justify="space-between">
|
||||
<Text fz="h2">
|
||||
{fmtOperation(update.operation)}{" "}
|
||||
{!versionIsNone(update.version) && fmtVersion(update.version)}
|
||||
</Text>
|
||||
<ActionIcon size="lg" variant="filled" color="red" onClick={close}>
|
||||
<ICONS.Clear size="1.3rem" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/** DETAILS */}
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function UpdatesSection({
|
||||
return (
|
||||
<Section
|
||||
title="Updates"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Update size="1.3rem" />}
|
||||
actions={
|
||||
link && (
|
||||
|
||||
+11
-13
@@ -141,13 +141,13 @@ export const swarmTaskStateIntention: (
|
||||
};
|
||||
|
||||
export const serverStateIntention: (
|
||||
state?: Types.ServerState,
|
||||
hasVersionMismatch?: boolean,
|
||||
) => ColorIntention = (state, hasVersionMismatch) => {
|
||||
state: Types.ServerState | undefined,
|
||||
versionMismatch: boolean,
|
||||
) => ColorIntention = (state, versionMismatch) => {
|
||||
switch (state) {
|
||||
case Types.ServerState.Ok:
|
||||
// If there's a version mismatch and the server is "Ok", show warning instead
|
||||
return hasVersionMismatch ? "Warning" : "Good";
|
||||
return versionMismatch ? "Warning" : "Good";
|
||||
case Types.ServerState.NotOk:
|
||||
return "Critical";
|
||||
case Types.ServerState.Disabled:
|
||||
@@ -157,18 +157,17 @@ export const serverStateIntention: (
|
||||
}
|
||||
};
|
||||
|
||||
export const stackStateIntention = (state?: Types.StackState) => {
|
||||
export const stackStateIntention = (
|
||||
state: Types.StackState | undefined,
|
||||
updateAvailable: boolean | undefined,
|
||||
) => {
|
||||
switch (state) {
|
||||
case undefined:
|
||||
return "None";
|
||||
case Types.StackState.Deploying:
|
||||
return "Warning";
|
||||
case Types.StackState.Running:
|
||||
return "Good";
|
||||
case Types.StackState.Paused:
|
||||
return "Warning";
|
||||
case Types.StackState.Stopped:
|
||||
return "Critical";
|
||||
return updateAvailable ? "Warning" : "Good";
|
||||
case Types.StackState.Restarting:
|
||||
return "Critical";
|
||||
case Types.StackState.Down:
|
||||
@@ -181,7 +180,8 @@ export const stackStateIntention = (state?: Types.StackState) => {
|
||||
};
|
||||
|
||||
export const deploymentStateIntention: (
|
||||
state?: Types.DeploymentState,
|
||||
state: Types.DeploymentState | undefined,
|
||||
updateAvailable: boolean | undefined,
|
||||
) => ColorIntention = (state) => {
|
||||
switch (state) {
|
||||
case undefined:
|
||||
@@ -192,8 +192,6 @@ export const deploymentStateIntention: (
|
||||
return "Good";
|
||||
case Types.DeploymentState.NotDeployed:
|
||||
return "Neutral";
|
||||
case Types.DeploymentState.Paused:
|
||||
return "Warning";
|
||||
case Types.DeploymentState.Unknown:
|
||||
return "Unknown";
|
||||
default:
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Alerts() {
|
||||
icon={ICONS.Alert}
|
||||
description="View historical alerts"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Stack>
|
||||
{/* QUERY */}
|
||||
<Group>
|
||||
{/* RESOURCE TYPE */}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function Containers() {
|
||||
icon={ICONS.Container}
|
||||
description="See all containers across all servers."
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<MultiSelect
|
||||
placeholder="Filter by Servers"
|
||||
|
||||
@@ -75,7 +75,13 @@ export default function DashboardActiveResources() {
|
||||
state: (
|
||||
<StatusBadge
|
||||
text={stack.info.state}
|
||||
intent={stackStateIntention(stack.info.state)}
|
||||
intent={stackStateIntention(
|
||||
stack.info.state,
|
||||
stack.info.services &&
|
||||
!stack.info.services.every(
|
||||
(service) => !service.update_available,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
@@ -85,7 +91,10 @@ export default function DashboardActiveResources() {
|
||||
state: (
|
||||
<StatusBadge
|
||||
text={deployment.info.state}
|
||||
intent={deploymentStateIntention(deployment.info.state)}
|
||||
intent={deploymentStateIntention(
|
||||
deployment.info.state,
|
||||
deployment.info.update_available,
|
||||
)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
@@ -147,6 +156,7 @@ export default function DashboardActiveResources() {
|
||||
return (
|
||||
<Section
|
||||
title="Active"
|
||||
mb="xl"
|
||||
icon={
|
||||
<Circle
|
||||
size="1rem"
|
||||
|
||||
@@ -27,7 +27,7 @@ import ServerStatsCard from "@/resources/server/stats-card";
|
||||
|
||||
export default function DashboardRecents() {
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Stack>
|
||||
<DashboardNoResources />
|
||||
{SIDEBAR_RESOURCES.map((type) => (
|
||||
<RecentRow key={type} type={type} />
|
||||
|
||||
+108
-108
@@ -8,9 +8,10 @@ import ResourceSubPage from "@/resources/sub-page";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import ConfirmButton from "@/ui/confirm-button";
|
||||
import { DataTable } from "@/ui/data-table";
|
||||
import PageGuard from "@/ui/page-guard";
|
||||
import Section from "@/ui/section";
|
||||
import ShowHideButton from "@/ui/show-hide-button";
|
||||
import { Box, Center, Group, Loader, Text } from "@mantine/core";
|
||||
import { Box, Center, Group, Text } from "@mantine/core";
|
||||
import { Types } from "komodo_client";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
@@ -79,132 +80,131 @@ function ImageInner({
|
||||
},
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Center h="30vh">
|
||||
<Loader size="xl" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Center h="30vh">
|
||||
<Text>Failed to inspect image.</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
return (
|
||||
<Center h="30vh">
|
||||
<Text>No image found with given name: {imageName}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const unused = containers && containers.length === 0 ? true : false;
|
||||
const intention = unused ? "Critical" : "Good";
|
||||
|
||||
return (
|
||||
<ResourceSubPage
|
||||
entityTypeName="Image"
|
||||
parentType="Server"
|
||||
parentId={serverId}
|
||||
name={imageName}
|
||||
icon={ICONS.Image}
|
||||
intent={intention}
|
||||
state={unused ? "Unused" : "In Use"}
|
||||
info={
|
||||
image.Id && (
|
||||
<Group gap="xs">
|
||||
<Text>Id:</Text>
|
||||
<Text title={image.Id} maw={150} className="text-ellipsis">
|
||||
{image.Id}
|
||||
</Text>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
executions={
|
||||
unused && (
|
||||
<ConfirmButton
|
||||
variant="filled"
|
||||
color="red"
|
||||
icon={<ICONS.Delete size="1rem" />}
|
||||
loading={deletePending}
|
||||
onClick={() => deleteImage({ server: serverId, name: imageName })}
|
||||
>
|
||||
Delete Image
|
||||
</ConfirmButton>
|
||||
)
|
||||
<PageGuard
|
||||
isPending={isPending}
|
||||
error={
|
||||
isError
|
||||
? "Failed to inspect image."
|
||||
: !image
|
||||
? "No image found with name: " + imageName
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{containers && containers.length > 0 && (
|
||||
<ContainersSection serverId={serverId} containers={containers} />
|
||||
)}
|
||||
|
||||
{/* TOP LEVEL IMAGE INFO */}
|
||||
<Section title="Details" icon={<ICONS.Info size="1.3rem" />}>
|
||||
<DataTable
|
||||
tableKey="image-info"
|
||||
data={[image]}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Architecture",
|
||||
header: "Architecture",
|
||||
},
|
||||
{
|
||||
accessorKey: "Os",
|
||||
header: "Os",
|
||||
},
|
||||
{
|
||||
accessorKey: "Size",
|
||||
header: "Size",
|
||||
cell: ({ row }) =>
|
||||
row.original.Size ? fmtSizeBytes(row.original.Size) : "Unknown",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{history && history.length > 0 && (
|
||||
<Section
|
||||
title="History"
|
||||
icon={<ICONS.History size="1.3rem" />}
|
||||
titleRight={
|
||||
<Box pl="md">
|
||||
<ShowHideButton show={showHistory} setShow={setShowHistory} />
|
||||
</Box>
|
||||
{image && (
|
||||
<ResourceSubPage
|
||||
entityTypeName="Image"
|
||||
parentType="Server"
|
||||
parentId={serverId}
|
||||
name={imageName}
|
||||
icon={ICONS.Image}
|
||||
intent={intention}
|
||||
state={unused ? "Unused" : "In Use"}
|
||||
info={
|
||||
image.Id && (
|
||||
<Group gap="xs">
|
||||
<Text>Id:</Text>
|
||||
<Text title={image.Id} maw={150} className="text-ellipsis">
|
||||
{image.Id}
|
||||
</Text>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
executions={
|
||||
unused && (
|
||||
<ConfirmButton
|
||||
variant="filled"
|
||||
color="red"
|
||||
icon={<ICONS.Delete size="1rem" />}
|
||||
loading={deletePending}
|
||||
onClick={() =>
|
||||
deleteImage({ server: serverId, name: imageName })
|
||||
}
|
||||
>
|
||||
Delete Image
|
||||
</ConfirmButton>
|
||||
)
|
||||
}
|
||||
>
|
||||
{showHistory && (
|
||||
{containers && containers.length > 0 && (
|
||||
<ContainersSection serverId={serverId} containers={containers} />
|
||||
)}
|
||||
|
||||
{/* TOP LEVEL IMAGE INFO */}
|
||||
<Section
|
||||
title="Details"
|
||||
icon={<ICONS.Info size="1.3rem" />}
|
||||
|
||||
>
|
||||
<DataTable
|
||||
tableKey="image-history"
|
||||
data={history.toReversed()}
|
||||
tableKey="image-info"
|
||||
data={[image]}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "CreatedBy",
|
||||
header: "Created By",
|
||||
size: 400,
|
||||
accessorKey: "Architecture",
|
||||
header: "Architecture",
|
||||
},
|
||||
{
|
||||
accessorKey: "Created",
|
||||
header: "Timestamp",
|
||||
accessorKey: "Os",
|
||||
header: "Os",
|
||||
},
|
||||
{
|
||||
accessorKey: "Size",
|
||||
header: "Size",
|
||||
cell: ({ row }) =>
|
||||
fmtDateWithMinutes(new Date(row.original.Created * 1000)),
|
||||
size: 200,
|
||||
row.original.Size
|
||||
? fmtSizeBytes(row.original.Size)
|
||||
: "Unknown",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{history && history.length > 0 && (
|
||||
<Section
|
||||
title="History"
|
||||
icon={<ICONS.History size="1.3rem" />}
|
||||
titleRight={
|
||||
<Box pl="md">
|
||||
<ShowHideButton show={showHistory} setShow={setShowHistory} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{showHistory && (
|
||||
<DataTable
|
||||
tableKey="image-history"
|
||||
data={history.toReversed()}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "CreatedBy",
|
||||
header: "Created By",
|
||||
size: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: "Created",
|
||||
header: "Timestamp",
|
||||
cell: ({ row }) =>
|
||||
fmtDateWithMinutes(
|
||||
new Date(row.original.Created * 1000),
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{specific.includes(Types.SpecificPermission.Inspect) && (
|
||||
<InspectSection json={image} showToggle />
|
||||
)}
|
||||
{specific.includes(Types.SpecificPermission.Inspect) && (
|
||||
<InspectSection json={image} showToggle />
|
||||
)}
|
||||
|
||||
<DockerLabelsSection labels={image?.Config?.Labels} />
|
||||
</ResourceSubPage>
|
||||
<DockerLabelsSection labels={image?.Config?.Labels} />
|
||||
</ResourceSubPage>
|
||||
)}
|
||||
</PageGuard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { komodo_client, useLoginOptions } from "@/lib/hooks";
|
||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||
import { MoghAuth } from "komodo_client";
|
||||
import { KeyRound } from "lucide-react";
|
||||
|
||||
export default function LoginHeader({
|
||||
secondFactorPending,
|
||||
}: {
|
||||
secondFactorPending: boolean;
|
||||
}) {
|
||||
const options = useLoginOptions().data;
|
||||
return (
|
||||
<Group gap="4rem" justify="space-between">
|
||||
<Group gap="sm">
|
||||
<img src="/mogh-512x512.png" width={42} height={42} alt="moghtech" />
|
||||
<Stack gap="0">
|
||||
<Text fz="h2" fw="450" lts="0.1rem">
|
||||
KOMODO
|
||||
</Text>
|
||||
<Text size="md" opacity={0.6} mt={-8}>
|
||||
Log In
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
{(
|
||||
[
|
||||
[options?.oidc, "Oidc"],
|
||||
[options?.github, "Github"],
|
||||
[options?.google, "Google"],
|
||||
] as Array<
|
||||
[boolean | undefined, MoghAuth.Types.ExternalLoginProvider]
|
||||
>
|
||||
).map(
|
||||
([enabled, provider]) =>
|
||||
enabled && (
|
||||
<Button
|
||||
key={provider}
|
||||
onClick={() => komodo_client().auth.externalLogin(provider)}
|
||||
leftSection={
|
||||
provider === "Oidc" ? (
|
||||
<KeyRound size="1rem" />
|
||||
) : (
|
||||
<img
|
||||
src={`/icons/${provider.toLowerCase()}.svg`}
|
||||
alt={provider}
|
||||
style={{ width: "1rem", height: "1rem" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
w={110}
|
||||
disabled={secondFactorPending}
|
||||
>
|
||||
{provider}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
komodo_client,
|
||||
useLogin,
|
||||
useLoginOptions,
|
||||
useUserInvalidate,
|
||||
} from "@/lib/hooks";
|
||||
import { useLogin, useLoginOptions, useUserInvalidate } from "@/lib/hooks";
|
||||
import { sanitizeQuery } from "@/lib/utils";
|
||||
import {
|
||||
Button,
|
||||
@@ -12,15 +7,15 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { MoghAuth } from "komodo_client";
|
||||
import { KeyRound } from "lucide-react";
|
||||
import { AlertTriangle, KeyRound } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import LoginHeader from "./header";
|
||||
|
||||
export default function Login({
|
||||
passkeyIsPending: _passkeyIsPending,
|
||||
@@ -101,11 +96,11 @@ export default function Login({
|
||||
},
|
||||
);
|
||||
|
||||
// const noAuthConfigured =
|
||||
// options !== undefined &&
|
||||
// Object.values(options).every((value) => value === false);
|
||||
const noAuthConfigured =
|
||||
options !== undefined &&
|
||||
Object.values(options).every((value) => value === false);
|
||||
|
||||
// const showSignUp = options !== undefined && !options.registration_disabled;
|
||||
const showSignUp = options !== undefined && !options.registration_disabled;
|
||||
|
||||
const localForm = useForm({
|
||||
mode: "uncontrolled",
|
||||
@@ -131,67 +126,10 @@ export default function Login({
|
||||
},
|
||||
});
|
||||
|
||||
const registration_disabled = options?.registration_disabled ?? true;
|
||||
|
||||
return (
|
||||
<Center h="80vh">
|
||||
<Fieldset
|
||||
legend={
|
||||
<Group gap="4rem" justify="space-between">
|
||||
<Group gap="sm">
|
||||
<img
|
||||
src="/mogh-512x512.png"
|
||||
width={42}
|
||||
height={42}
|
||||
alt="moghtech"
|
||||
/>
|
||||
<Stack gap="0">
|
||||
<Text fz="h2" fw="450" lts="0.1rem">
|
||||
KOMODO
|
||||
</Text>
|
||||
<Text size="md" opacity={0.6} mt={-8}>
|
||||
Log In
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
{(
|
||||
[
|
||||
[options?.oidc, "Oidc"],
|
||||
[options?.github, "Github"],
|
||||
[options?.google, "Google"],
|
||||
] as Array<
|
||||
[boolean | undefined, MoghAuth.Types.ExternalLoginProvider]
|
||||
>
|
||||
).map(
|
||||
([enabled, provider]) =>
|
||||
enabled && (
|
||||
<Button
|
||||
key={provider}
|
||||
onClick={() =>
|
||||
komodo_client().auth.externalLogin(provider)
|
||||
}
|
||||
leftSection={
|
||||
provider === "Oidc" ? (
|
||||
<KeyRound size="1rem" />
|
||||
) : (
|
||||
<img
|
||||
src={`/icons/${provider.toLowerCase()}.svg`}
|
||||
alt={provider}
|
||||
style={{ width: "1rem", height: "1rem" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
w={110}
|
||||
disabled={secondFactorPending}
|
||||
>
|
||||
{provider}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
}
|
||||
legend={<LoginHeader secondFactorPending={secondFactorPending} />}
|
||||
component="form"
|
||||
onSubmit={
|
||||
totpIsPending
|
||||
@@ -218,7 +156,7 @@ export default function Login({
|
||||
key={localForm.key("password")}
|
||||
/>
|
||||
<Group mt="sm" justify="end">
|
||||
{!registration_disabled && (
|
||||
{showSignUp && (
|
||||
<Button
|
||||
variant="outline"
|
||||
w={110}
|
||||
@@ -228,11 +166,7 @@ export default function Login({
|
||||
Sign Up
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
w={110}
|
||||
type="submit"
|
||||
loading={loginPending}
|
||||
>
|
||||
<Button w={110} type="submit" loading={loginPending}>
|
||||
Log In
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -240,13 +174,11 @@ export default function Login({
|
||||
)}
|
||||
|
||||
{passkeyIsPending && (
|
||||
<>
|
||||
<Group justify="center" my="lg">
|
||||
<KeyRound size="1.5rem" />
|
||||
<Group>
|
||||
<Loader />
|
||||
<Text size="lg">Provide your passkey to finish login</Text>
|
||||
</Group>
|
||||
</>
|
||||
<Text size="lg">Provide your passkey to finish login...</Text>
|
||||
<Loader />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{totpIsPending && (
|
||||
@@ -276,6 +208,25 @@ export default function Login({
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{noAuthConfigured && (
|
||||
<Group my="lg">
|
||||
<AlertTriangle size="2rem" />
|
||||
<Text>
|
||||
No login methods have been configured. <br />
|
||||
See the{" "}
|
||||
<a
|
||||
href="https://github.com/moghtech/komodo/blob/main/config/core.config.toml"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover-underline"
|
||||
>
|
||||
<b>example config</b>
|
||||
</a>{" "}
|
||||
for information on configuring auth.
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Fieldset>
|
||||
</Center>
|
||||
);
|
||||
@@ -65,7 +65,7 @@ export default function Profile() {
|
||||
<Section
|
||||
title="Login"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Key size="1.2rem" />}
|
||||
withBorder
|
||||
>
|
||||
@@ -115,7 +115,7 @@ export default function Profile() {
|
||||
<Section
|
||||
title="2FA"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Key size="1.2rem" />}
|
||||
withBorder
|
||||
>
|
||||
|
||||
@@ -95,7 +95,7 @@ export const LinkedLogins = ({
|
||||
<Section
|
||||
title="Providers"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Provider size="1.2rem" />}
|
||||
withBorder
|
||||
>
|
||||
|
||||
@@ -92,7 +92,7 @@ function ResourceInner({ type, id }: { type: UsableResource; id: string }) {
|
||||
{canExecute && Object.keys(RC.Executions).length > 0 && (
|
||||
<Section
|
||||
title="Execute"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Execution size="1rem" />}
|
||||
my="md"
|
||||
>
|
||||
|
||||
+24
-22
@@ -11,7 +11,7 @@ import {
|
||||
import { ResourceComponents, UsableResource } from "@/resources";
|
||||
import { Types } from "komodo_client";
|
||||
import Page from "@/ui/page";
|
||||
import { Group } from "@mantine/core";
|
||||
import { Group, Stack } from "@mantine/core";
|
||||
import TableSkeleton from "@/ui/table-skeleton";
|
||||
import TemplateQuerySelector from "@/components/template-query-selector";
|
||||
import TagsFilter from "@/components/tags/filter";
|
||||
@@ -72,29 +72,31 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
{(is_admin || !disable_non_admin_create) && <RC.New />}
|
||||
<RC.BatchExecutions />
|
||||
<Stack>
|
||||
<Group justify="space-between" w="100%">
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
{(is_admin || !disable_non_admin_create) && <RC.New />}
|
||||
<RC.BatchExecutions />
|
||||
</Group>
|
||||
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
{(type === "Stack" || type === "Deployment") && (
|
||||
<LabelledSwitch
|
||||
label="Pending Update"
|
||||
checked={filterUpdateAvailable}
|
||||
onCheckedChange={toggleFilterUpdateAvailable}
|
||||
opacity={0.7}
|
||||
fz="sm"
|
||||
/>
|
||||
)}
|
||||
<TemplateQuerySelector />
|
||||
<TagsFilter />
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
{(type === "Stack" || type === "Deployment") && (
|
||||
<LabelledSwitch
|
||||
label="Pending Update"
|
||||
checked={filterUpdateAvailable}
|
||||
onCheckedChange={toggleFilterUpdateAvailable}
|
||||
opacity={0.7}
|
||||
fz="sm"
|
||||
/>
|
||||
)}
|
||||
<TemplateQuerySelector />
|
||||
<TagsFilter />
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filtered ? <RC.Table resources={filtered ?? []} /> : <TableSkeleton />}
|
||||
{filtered ? <RC.Table resources={filtered ?? []} /> : <TableSkeleton />}
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+73
-71
@@ -14,7 +14,7 @@ import { ICONS } from "@/theme/icons";
|
||||
import { DataTable, SortableHeader } from "@/ui/data-table";
|
||||
import Page from "@/ui/page";
|
||||
import SearchInput from "@/ui/search-input";
|
||||
import { Group, Switch } from "@mantine/core";
|
||||
import { Group, Stack, Switch } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -31,78 +31,80 @@ export default function Schedules() {
|
||||
title="Schedules"
|
||||
description="See an overview of your scheduled Actions and Procedures."
|
||||
>
|
||||
<Group justify="end">
|
||||
<TagsFilter />
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
<Stack>
|
||||
<Group justify="end">
|
||||
<TagsFilter />
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
|
||||
<DataTable
|
||||
tableKey="schedules"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Target" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceLink
|
||||
type={row.original.target.type as UsableResource}
|
||||
id={row.original.target.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "schedule",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Schedule" />
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "next_scheduled_run",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Next Run" />
|
||||
),
|
||||
sortingFn: (a, b) => {
|
||||
const sa = a.original.next_scheduled_run;
|
||||
const sb = b.original.next_scheduled_run;
|
||||
|
||||
if (!sa && !sb) return 0;
|
||||
if (!sa) return 1;
|
||||
if (!sb) return -1;
|
||||
|
||||
if (sa > sb) return 1;
|
||||
else if (sa < sb) return -1;
|
||||
else return 0;
|
||||
<DataTable
|
||||
tableKey="schedules"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Target" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceLink
|
||||
type={row.original.target.type as UsableResource}
|
||||
id={row.original.target.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
row.original.next_scheduled_run
|
||||
? new Date(row.original.next_scheduled_run).toLocaleString()
|
||||
: "Not Scheduled",
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
accessorKey: "enabled",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Enabled" />
|
||||
),
|
||||
cell: ({ row: { original: schedule } }) => (
|
||||
<ScheduleEnableSwitch
|
||||
type={schedule.target.type as UsableResource}
|
||||
id={schedule.target.id}
|
||||
enabled={schedule.enabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
cell: ({ row }) => <TableTags tagIds={row.original.tags} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "schedule",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Schedule" />
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "next_scheduled_run",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Next Run" />
|
||||
),
|
||||
sortingFn: (a, b) => {
|
||||
const sa = a.original.next_scheduled_run;
|
||||
const sb = b.original.next_scheduled_run;
|
||||
|
||||
if (!sa && !sb) return 0;
|
||||
if (!sa) return 1;
|
||||
if (!sb) return -1;
|
||||
|
||||
if (sa > sb) return 1;
|
||||
else if (sa < sb) return -1;
|
||||
else return 0;
|
||||
},
|
||||
cell: ({ row }) =>
|
||||
row.original.next_scheduled_run
|
||||
? new Date(row.original.next_scheduled_run).toLocaleString()
|
||||
: "Not Scheduled",
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
accessorKey: "enabled",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Enabled" />
|
||||
),
|
||||
cell: ({ row: { original: schedule } }) => (
|
||||
<ScheduleEnableSwitch
|
||||
type={schedule.target.type as UsableResource}
|
||||
id={schedule.target.id}
|
||||
enabled={schedule.enabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
cell: ({ row }) => <TableTags tagIds={row.original.tags} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function SettingsUserGroups({
|
||||
return (
|
||||
<Section
|
||||
title="User Groups"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Users size="1.3rem" />}
|
||||
titleRight={
|
||||
<Box ml="md">
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function SettingsUsers() {
|
||||
|
||||
<Section
|
||||
title="Users"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.User size="1.3rem" />}
|
||||
titleRight={
|
||||
<Group ml="md">
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function SwarmTaskTabs({
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs color={colorByIntention(intent)} value={view}>
|
||||
<Tabs color={colorByIntention(intent)} value={view} mt="lg">
|
||||
{View}
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ICONS } from "@/theme/icons";
|
||||
import { filterBySplit, terminalLink } from "@/lib/utils";
|
||||
import { DataTable, SortableHeader } from "@/ui/data-table";
|
||||
import Page from "@/ui/page";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import DeleteTerminal from "./delete";
|
||||
@@ -24,101 +24,103 @@ export default function Terminals() {
|
||||
icon={ICONS.Terminal}
|
||||
description="Manage Terminals across all your Servers."
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<CreateTerminal />
|
||||
<BatchDeleteAllTerminals
|
||||
refetch={refetch}
|
||||
noTerminals={!terminals?.length}
|
||||
/>
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<CreateTerminal />
|
||||
<BatchDeleteAllTerminals
|
||||
refetch={refetch}
|
||||
noTerminals={!terminals?.length}
|
||||
/>
|
||||
</Group>
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
<SearchInput value={search} onSearch={setSearch} />
|
||||
</Group>
|
||||
|
||||
<DataTable
|
||||
tableKey="terminals"
|
||||
data={filtered}
|
||||
loading={isPending}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={terminalLink(row.original)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Group className="hover-underline" fz="md" wrap="nowrap">
|
||||
<ICONS.Terminal size="1rem" />
|
||||
{row.original.name}
|
||||
</Group>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "target",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Target" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<TerminalTargetLink target={row.original.target} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "command",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Command" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Text ff="monospace" fz="sm">
|
||||
{row.original.command}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
accessorKey: "size",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Size" />
|
||||
),
|
||||
cell: ({
|
||||
row: {
|
||||
original: { stored_size_kb },
|
||||
},
|
||||
}) => (
|
||||
<span className="font-mono px-2 py-1 bg-secondary rounded-md">
|
||||
{stored_size_kb.toFixed()} KiB
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({
|
||||
row: {
|
||||
original: { created_at },
|
||||
},
|
||||
}) => fmtDateWithMinutes(new Date(created_at)),
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
cell: ({ row }) => (
|
||||
<DeleteTerminal
|
||||
target={row.original.target}
|
||||
terminal={row.original.name}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<DataTable
|
||||
tableKey="terminals"
|
||||
data={filtered}
|
||||
loading={isPending}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={terminalLink(row.original)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Group className="hover-underline" fz="md" wrap="nowrap">
|
||||
<ICONS.Terminal size="1rem" />
|
||||
{row.original.name}
|
||||
</Group>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
accessorKey: "target",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Target" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<TerminalTargetLink target={row.original.target} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "command",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Command" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Text ff="monospace" fz="sm">
|
||||
{row.original.command}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
accessorKey: "size",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Size" />
|
||||
),
|
||||
cell: ({
|
||||
row: {
|
||||
original: { stored_size_kb },
|
||||
},
|
||||
}) => (
|
||||
<span className="font-mono px-2 py-1 bg-secondary rounded-md">
|
||||
{stored_size_kb.toFixed()} KiB
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({
|
||||
row: {
|
||||
original: { created_at },
|
||||
},
|
||||
}) => fmtDateWithMinutes(new Date(created_at)),
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
cell: ({ row }) => (
|
||||
<DeleteTerminal
|
||||
target={row.original.target}
|
||||
terminal={row.original.name}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function Updates() {
|
||||
icon={ICONS.Update}
|
||||
description="View historical updates"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Stack>
|
||||
{/* QUERY */}
|
||||
<Group>
|
||||
{/* RESOURCE TYPE */}
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function UserGroup() {
|
||||
<Section
|
||||
title="Users"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.User size="1.2rem" />}
|
||||
titleRight={
|
||||
<Group ml="md">
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function User() {
|
||||
<Section
|
||||
title="User Permissions"
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
withBorder
|
||||
>
|
||||
<Group>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function UserMemberGroups({ userId }: { userId: string }) {
|
||||
title="Groups"
|
||||
icon={<ICONS.UserGroup size="1.2rem" />}
|
||||
titleFz="h3"
|
||||
titleMb="0"
|
||||
|
||||
titleRight={
|
||||
<Box ml="md">
|
||||
<UserAddUserGroup userId={userId} />
|
||||
|
||||
@@ -161,7 +161,7 @@ export default function ActionConfig({ id }: { id: string }) {
|
||||
: false
|
||||
}
|
||||
disabled={disabled || !(update.schedule ?? config.schedule)}
|
||||
onChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
onCheckedChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
/>
|
||||
),
|
||||
schedule_format: (schedule_format, set) => (
|
||||
@@ -262,7 +262,7 @@ export default function ActionConfig({ id }: { id: string }) {
|
||||
placeholder="Branch"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
w={200}
|
||||
w={{ base: "100%", sm: 200 }}
|
||||
disabled={branch === "__ANY__"}
|
||||
/>
|
||||
<LabelledSwitch
|
||||
|
||||
@@ -62,7 +62,6 @@ export default function BuildTabs({ id }: { id: string }) {
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -75,8 +75,14 @@ export const BuilderComponents: RequiredResourceComponents<
|
||||
? builder.info.instance_type
|
||||
: undefined,
|
||||
);
|
||||
const coreVersion = useRead("GetVersion", {}).data?.version;
|
||||
const intent = server?.info.state
|
||||
? serverStateIntention(server.info.state)
|
||||
? serverStateIntention(
|
||||
server.info.state,
|
||||
!!coreVersion &&
|
||||
!!server.info.version &&
|
||||
coreVersion !== server.info.version,
|
||||
)
|
||||
: "Neutral";
|
||||
return (
|
||||
<ResourceHeader
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function DeploymentConfig({
|
||||
label="Poll for Updates"
|
||||
description="Check for updates to the image during Global Auto Update."
|
||||
value={autoUpdate || poll}
|
||||
onChange={(poll_for_updates) => set({ poll_for_updates })}
|
||||
onCheckedChange={(poll_for_updates) => set({ poll_for_updates })}
|
||||
disabled={disabled || autoUpdate}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -103,11 +103,15 @@ export const DeploymentComponents: RequiredResourceComponents<
|
||||
Table: DeploymentTable,
|
||||
|
||||
Icon: ({ id, size = "1rem", noColor }) => {
|
||||
const state = useRead("ListDeployments", {}).data?.find((r) => r.id === id)
|
||||
?.info.state;
|
||||
const info = useRead("ListDeployments", {}).data?.find(
|
||||
(r) => r.id === id,
|
||||
)?.info;
|
||||
const color = noColor
|
||||
? undefined
|
||||
: state && hexColorByIntention(deploymentStateIntention(state));
|
||||
: info &&
|
||||
hexColorByIntention(
|
||||
deploymentStateIntention(info.state, info.update_available),
|
||||
);
|
||||
return <ICONS.Deployment size={size} color={color} />;
|
||||
},
|
||||
|
||||
@@ -118,7 +122,10 @@ export const DeploymentComponents: RequiredResourceComponents<
|
||||
type="Deployment"
|
||||
id={id}
|
||||
resource={deployment}
|
||||
intent={deploymentStateIntention(deployment?.info.state)}
|
||||
intent={deploymentStateIntention(
|
||||
deployment?.info.state,
|
||||
deployment?.info.update_available,
|
||||
)}
|
||||
icon={ICONS.Deployment}
|
||||
name={deployment?.name}
|
||||
state={deployment?.info.state}
|
||||
@@ -128,9 +135,12 @@ export const DeploymentComponents: RequiredResourceComponents<
|
||||
},
|
||||
|
||||
State: ({ id }) => {
|
||||
let state = useDeployment(id)?.info.state;
|
||||
let info = useDeployment(id)?.info;
|
||||
return (
|
||||
<StatusBadge text={state} intent={deploymentStateIntention(state)} />
|
||||
<StatusBadge
|
||||
text={info?.state}
|
||||
intent={deploymentStateIntention(info?.state, info?.update_available)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Info: {
|
||||
|
||||
@@ -91,7 +91,6 @@ export default function DeploymentTabs({ id }: { id: string }) {
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -130,7 +129,9 @@ export default function DeploymentTabs({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
color={colorByIntention(deploymentStateIntention(state))}
|
||||
color={colorByIntention(
|
||||
deploymentStateIntention(state, info?.update_available),
|
||||
)}
|
||||
value={view}
|
||||
>
|
||||
{View}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function NewResource<Config>({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
placeholder="Template (Optional)"
|
||||
placeholder="Select Template"
|
||||
targetProps={{ w: "100%", maw: "100%" }}
|
||||
width="target"
|
||||
position="bottom"
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function ProcedureConfig({ id }: { id: string }) {
|
||||
: false
|
||||
}
|
||||
disabled={disabled || !(update.schedule ?? config.schedule)}
|
||||
onChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
onCheckedChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
/>
|
||||
),
|
||||
schedule_format: (schedule_format, set) => (
|
||||
@@ -276,7 +276,7 @@ export default function ProcedureConfig({ id }: { id: string }) {
|
||||
placeholder="Branch"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
w={200}
|
||||
w={{ base: "100%", sm: 200 }}
|
||||
disabled={branch === "__ANY__"}
|
||||
/>
|
||||
<LabelledSwitch
|
||||
|
||||
@@ -58,7 +58,6 @@ export default function RepoTabs({ id }: { id: string }) {
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import ServerNetworks from "./networks";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import ServerVolumes from "./volumes";
|
||||
import ServerImages from "./images";
|
||||
import { useRead } from "@/lib/hooks";
|
||||
|
||||
type ServerDockerView = "Containers" | "Networks" | "Volumes" | "Images";
|
||||
|
||||
@@ -30,7 +31,9 @@ export default function ServerDockerResources({
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
}) {
|
||||
const state = useServer(id)?.info.state ?? Types.ServerState.NotOk;
|
||||
const coreVersion = useRead("GetVersion", {}).data?.version;
|
||||
const info = useServer(id)?.info;
|
||||
const state = info?.state ?? Types.ServerState.NotOk;
|
||||
const [view, setView] = useLocalStorage<ServerDockerView>({
|
||||
key: "server-info-view-v1",
|
||||
defaultValue: "Containers",
|
||||
@@ -98,7 +101,15 @@ export default function ServerDockerResources({
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<Tabs color={colorByIntention(serverStateIntention(state))} value={view}>
|
||||
<Tabs
|
||||
color={colorByIntention(
|
||||
serverStateIntention(
|
||||
state,
|
||||
!!coreVersion && !!info?.version && coreVersion !== info.version,
|
||||
),
|
||||
)}
|
||||
value={view}
|
||||
>
|
||||
{View}
|
||||
</Tabs>
|
||||
</Section>
|
||||
|
||||
@@ -133,8 +133,17 @@ export const ServerComponents: RequiredResourceComponents<
|
||||
},
|
||||
|
||||
State: ({ id }) => {
|
||||
let state = useServer(id)?.info.state;
|
||||
return <StatusBadge text={state} intent={serverStateIntention(state)} />;
|
||||
const coreVersion = useRead("GetVersion", {}).data?.version;
|
||||
const info = useServer(id)?.info;
|
||||
return (
|
||||
<StatusBadge
|
||||
text={info?.state}
|
||||
intent={serverStateIntention(
|
||||
info?.state,
|
||||
!!coreVersion && !!info?.version && coreVersion !== info.version,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Info: {
|
||||
ServerVersion,
|
||||
|
||||
@@ -39,10 +39,10 @@ type StatType =
|
||||
type StatDatapoint = { date: number; value: number };
|
||||
|
||||
const STAT_TYPES: [StatType, ReactNode][] = [
|
||||
["Load Average", <ICONS.LoadAvg size="1.1rem" />],
|
||||
["Cpu", <ICONS.Cpu size="1.1rem" />],
|
||||
["Memory", <ICONS.Memory size="1.1rem" />],
|
||||
["Disk", <ICONS.Disk size="1.1rem" />],
|
||||
["Load Average", <ICONS.LoadAvg size="1.1rem" />],
|
||||
["Network Ingress", <Download size="1.1rem" />],
|
||||
["Network Egress", <Upload size="1.1rem" />],
|
||||
];
|
||||
@@ -204,7 +204,7 @@ function StatChart({
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Group c="dimmed" gap="xs">
|
||||
<Group gap="xs">
|
||||
{icon}
|
||||
<Text fz="xl" fw={500}>
|
||||
{type}
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function ServerTabs({ id }: { id: string }) {
|
||||
|
||||
const { specificTerminal } = usePermissions({ type: "Server", id });
|
||||
|
||||
const coreVersion = useRead("GetVersion", {}).data?.version;
|
||||
const serverInfo = useServer(id)?.info;
|
||||
const notReachable = serverInfo?.state !== Types.ServerState.Ok;
|
||||
const terminalDisabled =
|
||||
@@ -83,7 +84,6 @@ export default function ServerTabs({ id }: { id: string }) {
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -126,7 +126,14 @@ export default function ServerTabs({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
color={colorByIntention(serverStateIntention(serverInfo?.state))}
|
||||
color={colorByIntention(
|
||||
serverStateIntention(
|
||||
serverInfo?.state,
|
||||
!!coreVersion &&
|
||||
!!serverInfo?.version &&
|
||||
coreVersion !== serverInfo.version,
|
||||
),
|
||||
)}
|
||||
value={view}
|
||||
>
|
||||
{View}
|
||||
|
||||
@@ -371,7 +371,7 @@ export default function StackConfig({
|
||||
label="Poll for Updates"
|
||||
description="Check for updates to the image during Global Auto Update."
|
||||
value={auto_update || poll}
|
||||
onChange={(poll_for_updates) => set({ poll_for_updates })}
|
||||
onCheckedChange={(poll_for_updates) => set({ poll_for_updates })}
|
||||
disabled={disabled || auto_update}
|
||||
/>
|
||||
);
|
||||
@@ -385,7 +385,7 @@ export default function StackConfig({
|
||||
label="Full Stack Auto Update"
|
||||
description="Always redeploy full stack instead of just specific services with update."
|
||||
value={value}
|
||||
onChange={(auto_update_all_services) =>
|
||||
onCheckedChange={(auto_update_all_services) =>
|
||||
set({ auto_update_all_services })
|
||||
}
|
||||
disabled={disabled || !auto_update}
|
||||
|
||||
@@ -109,11 +109,17 @@ export const StackComponents: RequiredResourceComponents<
|
||||
Table: StackTable,
|
||||
|
||||
Icon: ({ id, size = "1rem", noColor }) => {
|
||||
const state = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info
|
||||
.state;
|
||||
const info = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info;
|
||||
const color = noColor
|
||||
? undefined
|
||||
: state && hexColorByIntention(stackStateIntention(state));
|
||||
: info &&
|
||||
hexColorByIntention(
|
||||
stackStateIntention(
|
||||
info.state,
|
||||
info.services &&
|
||||
!info.services.every((service) => !service.update_available),
|
||||
),
|
||||
);
|
||||
return <ICONS.Stack size={size} color={color} />;
|
||||
},
|
||||
|
||||
@@ -124,7 +130,11 @@ export const StackComponents: RequiredResourceComponents<
|
||||
type="Stack"
|
||||
id={id}
|
||||
resource={stack}
|
||||
intent={stackStateIntention(stack?.info.state)}
|
||||
intent={stackStateIntention(
|
||||
stack?.info.state,
|
||||
stack?.info.services &&
|
||||
!stack?.info.services.every((service) => !service.update_available),
|
||||
)}
|
||||
icon={ICONS.Stack}
|
||||
name={stack?.name}
|
||||
state={stack?.info.state}
|
||||
@@ -138,8 +148,17 @@ export const StackComponents: RequiredResourceComponents<
|
||||
},
|
||||
|
||||
State: ({ id }) => {
|
||||
let state = useStack(id)?.info.state;
|
||||
return <StatusBadge text={state} intent={stackStateIntention(state)} />;
|
||||
let info = useStack(id)?.info;
|
||||
return (
|
||||
<StatusBadge
|
||||
text={info?.state}
|
||||
intent={stackStateIntention(
|
||||
info?.state,
|
||||
info?.services &&
|
||||
!info.services.every((service) => !service.update_available),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Info: {
|
||||
DeployTarget: ({ id }) => {
|
||||
|
||||
@@ -95,7 +95,6 @@ export default function StackTabs({ id }: { id: string }) {
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -135,7 +134,16 @@ export default function StackTabs({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs color={colorByIntention(stackStateIntention(state))} value={view}>
|
||||
<Tabs
|
||||
color={colorByIntention(
|
||||
stackStateIntention(
|
||||
state,
|
||||
info?.services &&
|
||||
!info.services.every((service) => !service.update_available),
|
||||
),
|
||||
)}
|
||||
value={view}
|
||||
>
|
||||
{View}
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function ResourceSubPage({
|
||||
{canExecute && executions && (
|
||||
<Section
|
||||
title="Execute"
|
||||
titleMb="0"
|
||||
|
||||
icon={<ICONS.Execution size="1.3rem" />}
|
||||
my="md"
|
||||
>
|
||||
|
||||
@@ -76,6 +76,7 @@ export default function SwarmConfig({
|
||||
}
|
||||
disabled={disabled}
|
||||
targetProps={{ w: "90%", maw: "" }}
|
||||
clearable={false}
|
||||
/>
|
||||
{!disabled && (
|
||||
<ActionIcon variant="filled" color="red">
|
||||
|
||||
@@ -21,8 +21,7 @@ export default function JoinSwarmCommands({
|
||||
return (
|
||||
<Stack>
|
||||
<Text c="dimmed">
|
||||
Copy a command below and run it on the target host to join it to the
|
||||
swarm.
|
||||
Copy a command below and run it on the target host to join the swarm.
|
||||
</Text>
|
||||
|
||||
{addr && tokens ? (
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function SwarmTabs({ id }: { id: string }) {
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function ResourceSyncConfig({
|
||||
label="Delete Unmatched Resources"
|
||||
description="Executions will delete any resources not found in the resource files. Only use this when using one sync for everything."
|
||||
value={managed || delete_mode}
|
||||
onChange={(delete_mode) => set({ delete: delete_mode })}
|
||||
onCheckedChange={(delete_mode) => set({ delete: delete_mode })}
|
||||
disabled={disabled || managed}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,6 @@ export default function ResourceSyncTabs({ id }: { id: string }) {
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, Fragment, SetStateAction } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { ConfigFieldArgs, ConfigGroupArgs } from ".";
|
||||
import { ConfigInput, ConfigSwitch } from "./item";
|
||||
import { Group } from "@mantine/core";
|
||||
@@ -13,7 +13,7 @@ export default function ConfigGroup<T>({
|
||||
}: {
|
||||
config: T;
|
||||
update: Partial<T>;
|
||||
setUpdate: Dispatch<SetStateAction<Partial<T>>>;
|
||||
setUpdate: (update: Partial<T>) => void;
|
||||
disabled: boolean;
|
||||
fields: ConfigGroupArgs<T>["fields"];
|
||||
}) {
|
||||
@@ -38,7 +38,9 @@ export default function ConfigGroup<T>({
|
||||
key={key}
|
||||
label={args?.label ?? key}
|
||||
value={value}
|
||||
onChange={(value) => setUpdate({ [key]: value } as Partial<T>)}
|
||||
onValueChange={(value) =>
|
||||
setUpdate({ [key]: value } as Partial<T>)
|
||||
}
|
||||
disabled={args?.disabled || disabled}
|
||||
placeholder={args?.placeholder}
|
||||
description={args?.description}
|
||||
@@ -52,7 +54,7 @@ export default function ConfigGroup<T>({
|
||||
key={key}
|
||||
label={args?.label ?? key}
|
||||
value={Number(value)}
|
||||
onChange={(value) =>
|
||||
onValueChange={(value) =>
|
||||
setUpdate({ [key]: Number(value) } as Partial<T>)
|
||||
}
|
||||
disabled={args?.disabled || disabled}
|
||||
@@ -68,7 +70,9 @@ export default function ConfigGroup<T>({
|
||||
key={key}
|
||||
label={args?.label ?? key}
|
||||
value={value}
|
||||
onChange={(value) => setUpdate({ [key]: value } as Partial<T>)}
|
||||
onCheckedChange={(value) =>
|
||||
setUpdate({ [key]: value } as Partial<T>)
|
||||
}
|
||||
disabled={args?.disabled || disabled}
|
||||
description={args?.description}
|
||||
boldLabel={args?.boldLabel}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function Config<T>({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Stack gap="xl">
|
||||
<Stack>
|
||||
{(groupArgs as ConfigGroupArgs<T>[])
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(
|
||||
@@ -189,10 +189,10 @@ export default function Config<T>({
|
||||
|
||||
const SaveOrResetComponent = changesMade && (
|
||||
<>
|
||||
<Group visibleFrom="lg" justify="flex-end">
|
||||
<Group visibleFrom="xs" justify="flex-end">
|
||||
<SaveOrReset unsavedIndicator />
|
||||
</Group>
|
||||
<Stack hiddenFrom="lg">
|
||||
<Stack hiddenFrom="xs">
|
||||
<SaveOrReset unsavedIndicator fullWidth />
|
||||
</Stack>
|
||||
</>
|
||||
@@ -209,7 +209,7 @@ export default function Config<T>({
|
||||
{!disableSidebar && (
|
||||
<Flex
|
||||
w="100%"
|
||||
gap={{ base: "md", lg: "xl" }}
|
||||
gap="md"
|
||||
direction={{ base: "column", lg: "row" }}
|
||||
>
|
||||
{/** SIDEBAR (LG) */}
|
||||
|
||||
@@ -119,20 +119,20 @@ export function ConfigInput({
|
||||
export function ConfigSwitch({
|
||||
value,
|
||||
disabled,
|
||||
onChange,
|
||||
onCheckedChange,
|
||||
switchProps,
|
||||
...itemProps
|
||||
}: {
|
||||
value: boolean | undefined;
|
||||
disabled: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
onCheckedChange: (value: boolean) => void;
|
||||
switchProps?: SwitchProps;
|
||||
} & Omit<Omit<ConfigItemProps, "children">, "onChange">) {
|
||||
} & Omit<ConfigItemProps, "children">) {
|
||||
return (
|
||||
<ConfigItem {...itemProps}>
|
||||
<EnableSwitch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
{...switchProps}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,17 @@ export interface ConfirmButtonProps extends ButtonProps {
|
||||
const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
|
||||
forwardRef<HTMLButtonElement, ConfirmButtonProps>(
|
||||
(
|
||||
{ icon, rightSection, children, onClick, onBlur, miw, loading, ...props },
|
||||
{
|
||||
icon,
|
||||
rightSection,
|
||||
children,
|
||||
onClick,
|
||||
onBlur,
|
||||
miw,
|
||||
loading,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [clickedOnce, setClickedOnce] = useState(false);
|
||||
@@ -65,6 +75,7 @@ const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
|
||||
(rightSection ?? icon ?? <ICONS.Unknown size="1rem" />)
|
||||
)
|
||||
}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface ConfirmIconProps extends ActionIconProps {
|
||||
|
||||
const ConfirmIcon = createPolymorphicComponent<"button", ConfirmIconProps>(
|
||||
forwardRef<HTMLButtonElement, ConfirmIconProps>(
|
||||
({ children, onClick, onBlur, miw, loading, ...props }, ref) => {
|
||||
({ children, onClick, onBlur, miw, loading, disabled, ...props }, ref) => {
|
||||
const [clickedOnce, setClickedOnce] = useState(false);
|
||||
useEffect(() => {
|
||||
if (clickedOnce) {
|
||||
@@ -49,6 +49,7 @@ const ConfirmIcon = createPolymorphicComponent<"button", ConfirmIconProps>(
|
||||
onBlur?.(e);
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -131,7 +131,6 @@ export default function ConfirmModal({
|
||||
</Modal>
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
@@ -147,6 +146,7 @@ export default function ConfirmModal({
|
||||
)
|
||||
}
|
||||
loading={targetNoIcon ? loading : undefined}
|
||||
disabled={disabled || loading}
|
||||
{...targetProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -14,8 +14,8 @@ export default function EntityPage({
|
||||
...props
|
||||
}: EntityPageProps) {
|
||||
return (
|
||||
<Stack gap="xl" mb="50vh" {...props}>
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Stack mb="50vh" {...props}>
|
||||
<Group justify="space-between">
|
||||
<BackButton to={backTo} />
|
||||
{actions && <Group wrap="nowrap">{actions}</Group>}
|
||||
</Group>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function LabelledSwitch({
|
||||
}}
|
||||
className="bordered-light"
|
||||
px="xs"
|
||||
py={6}
|
||||
py={4}
|
||||
bdrs="sm"
|
||||
style={{ cursor: "pointer" }}
|
||||
justify="space-between"
|
||||
|
||||
@@ -77,12 +77,13 @@ export function MobileFriendlyTabsSelector({
|
||||
actions,
|
||||
value,
|
||||
onValueChange,
|
||||
changeAt = "md",
|
||||
changeAt: _changeAt,
|
||||
fullIconSize = "1.3rem",
|
||||
mobileIconSize = "1rem",
|
||||
tabProps,
|
||||
}: MobileFriendlyTabsSelectorProps) {
|
||||
const tabs = _tabs.filter((t) => !t.hidden);
|
||||
const changeAt = _changeAt ?? (tabs.length > 4 ? "lg" : "md");
|
||||
const SelectedIcon = tabs.find((tab) => tab.value === value)?.icon;
|
||||
return (
|
||||
<>
|
||||
@@ -102,6 +103,7 @@ export function MobileFriendlyTabsSelector({
|
||||
fz="h3"
|
||||
justify="center"
|
||||
c={tabValue === value ? undefined : "dimmed"}
|
||||
w={tabProps?.w ?? 140}
|
||||
{...tabProps}
|
||||
>
|
||||
{Icon && <Icon size={fullIconSize} />}
|
||||
|
||||
+1
-2
@@ -50,7 +50,6 @@ const Page = createPolymorphicComponent<"div", PageProps>(
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
px="xl"
|
||||
py="xs"
|
||||
mb="lg"
|
||||
style={{
|
||||
border: "1px solid var(--mantine-color-accent-border-0)",
|
||||
}}
|
||||
@@ -80,7 +79,7 @@ const Page = createPolymorphicComponent<"div", PageProps>(
|
||||
</Paper>
|
||||
);
|
||||
return (
|
||||
<Stack gap="xl" {...stackProps} ref={ref}>
|
||||
<Stack {...stackProps} ref={ref}>
|
||||
{aboveTitle}
|
||||
{oppositeTitle ? (
|
||||
<Group justify="space-between" align="start">
|
||||
|
||||
@@ -35,7 +35,7 @@ const Section = createPolymorphicComponent<"div", SectionProps>(
|
||||
(
|
||||
{
|
||||
titleFz = "h2",
|
||||
titleMb = { base: "0", lg: "sm" },
|
||||
titleMb,
|
||||
title,
|
||||
titleDimmed,
|
||||
titleNode,
|
||||
|
||||
Reference in New Issue
Block a user