mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-16 21:51:05 -05:00
Compare commits
21 Commits
v2.0.0-dev
...
v2.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aebd5ed92 | ||
|
|
a9352ad90b | ||
|
|
37bd221609 | ||
|
|
bf5975bb93 | ||
|
|
d7706a32c1 | ||
|
|
dea9c49772 | ||
|
|
95666e69d8 | ||
|
|
cbc008fc89 | ||
|
|
445fa42a01 | ||
|
|
2c2b0eda8f | ||
|
|
15eefa3385 | ||
|
|
cb9bc80346 | ||
|
|
8c682c091f | ||
|
|
ca021a3979 | ||
|
|
1480c3b020 | ||
|
|
42f452fdda | ||
|
|
8ded07dfe5 | ||
|
|
d51c1cb1e7 | ||
|
|
b5f184b286 | ||
|
|
294ef20019 | ||
|
|
3efcaa0740 |
31
Cargo.lock
generated
31
Cargo.lock
generated
@@ -1149,7 +1149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"shlex",
|
||||
@@ -1489,7 +1489,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -1759,7 +1759,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1801,7 +1801,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1930,7 +1930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"mogh_error",
|
||||
]
|
||||
@@ -2109,7 +2109,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2709,7 +2709,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2835,7 +2835,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2852,6 +2852,7 @@ dependencies = [
|
||||
"mogh_logger",
|
||||
"mogh_pki",
|
||||
"mogh_secret_file",
|
||||
"rustls 0.23.37",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
@@ -2863,7 +2864,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2902,7 +2903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2975,7 +2976,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3224,9 +3225,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mogh_auth_server"
|
||||
version = "1.2.11"
|
||||
version = "1.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5d93cfc5fd1041273091e6c55544a98df78497dd628503481d22cb676c90502"
|
||||
checksum = "e748cc16d2f7d9a86fbab3eff5f2e847df205db8ff5aa27728e45271f96d327b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -4030,7 +4031,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"encoding",
|
||||
@@ -6007,7 +6008,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-123"
|
||||
version = "2.0.0-dev-124"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -37,7 +37,7 @@ mogh_error = { version = "1.0.3", default-features = false }
|
||||
derive_default_builder = "0.1.8"
|
||||
async_timing_util = "1.1.0"
|
||||
mogh_auth_client = "1.2.2"
|
||||
mogh_auth_server = "1.2.11"
|
||||
mogh_auth_server = "1.2.12"
|
||||
mogh_secret_file = "1.0.1"
|
||||
mogh_validations = "1.0.1"
|
||||
mogh_rate_limit = "1.0.1"
|
||||
|
||||
@@ -33,6 +33,7 @@ colored.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
clap.workspace = true
|
||||
|
||||
@@ -12,6 +12,9 @@ mod config;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install default crypto provider");
|
||||
mogh_logger::init(&config::cli_config().cli_logging)?;
|
||||
let args = config::cli_args();
|
||||
let env = config::cli_env();
|
||||
|
||||
@@ -33,6 +33,7 @@ use komodo_client::{
|
||||
user::{action_user, system_user},
|
||||
},
|
||||
};
|
||||
use mogh_auth_server::api::login::local::sign_up_local_user;
|
||||
use mogh_resolver::Resolve;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -41,6 +42,7 @@ use crate::{
|
||||
execute::{ExecuteArgs, ExecuteRequest},
|
||||
write::WriteArgs,
|
||||
},
|
||||
auth::KomodoAuthImpl,
|
||||
config::core_config,
|
||||
helpers::update::init_execution_update,
|
||||
network, resource,
|
||||
@@ -301,7 +303,7 @@ async fn ensure_init_user_and_resources() {
|
||||
|
||||
// Assumes if there are any existing users, procedures, or tags,
|
||||
// the default procedures do not need to be set up.
|
||||
let Ok((None, None, None)) = tokio::try_join!(
|
||||
let Ok((None, procedures, tags)) = tokio::try_join!(
|
||||
db.users.find_one(Document::new()),
|
||||
db.procedures.find_one(Document::new()),
|
||||
db.tags.find_one(Document::new()),
|
||||
@@ -314,20 +316,16 @@ async fn ensure_init_user_and_resources() {
|
||||
// Init admin user if set in config.
|
||||
if let Some(username) = &config.init_admin_username {
|
||||
info!("Creating init admin user...");
|
||||
// if let Err(e) = (SignUpLocalUser {
|
||||
// username: username.clone(),
|
||||
// password: config.init_admin_password.clone(),
|
||||
// })
|
||||
// .resolve(&AuthArgs {
|
||||
// headers: Default::default(),
|
||||
// ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
// session: None,
|
||||
// })
|
||||
// .await
|
||||
// {
|
||||
// error!("Failed to create init admin user | {:#}", e.error);
|
||||
// return;
|
||||
// }
|
||||
if let Err(e) = sign_up_local_user(
|
||||
&KomodoAuthImpl,
|
||||
username.to_string(),
|
||||
&config.init_admin_password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to create init admin user | {:#}", e.error);
|
||||
return;
|
||||
}
|
||||
match db
|
||||
.users
|
||||
.find_one(doc! { "username": username })
|
||||
@@ -347,7 +345,10 @@ async fn ensure_init_user_and_resources() {
|
||||
}
|
||||
}
|
||||
|
||||
if config.disable_init_resources {
|
||||
if config.disable_init_resources
|
||||
|| procedures.is_some()
|
||||
|| tags.is_some()
|
||||
{
|
||||
info!("System resources init {}", "DISABLED".red());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"mogh_auth_client": "^1.2.0"
|
||||
"mogh_auth_client": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -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.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.2.0.tgz#d840abb85c967133570f247e0d671ac4054db3ff"
|
||||
integrity sha512-d60lSNzbOJcZwwSeyn7jLBmNI0FQTpXtOjR/OJj3M5KnUpmofpjlgGqLYGbVsZkoVipqohhi7aM2uVHEiicCVQ==
|
||||
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==
|
||||
dependencies:
|
||||
jwt-decode "^4.0.0"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const Sidebar = ({ close }: { close: () => void }) => {
|
||||
<Stack justify="space-between" gap="md" h="96%" m="xl" mt="24" mr="md">
|
||||
{/* TOP AREA (scrolling) */}
|
||||
<ScrollArea>
|
||||
<Stack gap="0.25rem" mr="md">
|
||||
<Stack gap="0.15rem" mr="md">
|
||||
<SidebarLink
|
||||
label="Dashboard"
|
||||
icon={<ICONS.Dashboard size="1rem" />}
|
||||
@@ -43,7 +43,7 @@ const Sidebar = ({ close }: { close: () => void }) => {
|
||||
Resources
|
||||
</Text>
|
||||
}
|
||||
my="xs"
|
||||
my="0.1rem"
|
||||
/>
|
||||
|
||||
{SIDEBAR_RESOURCES.map((type) => {
|
||||
@@ -65,7 +65,7 @@ const Sidebar = ({ close }: { close: () => void }) => {
|
||||
Notifications
|
||||
</Text>
|
||||
}
|
||||
my="xs"
|
||||
my="0.1rem"
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
|
||||
@@ -133,12 +133,12 @@ export default function NewApiKey({ userId }: { userId?: string }) {
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text>Key</Text>
|
||||
<CopyText content={created.key} label="api key" />
|
||||
<CopyText content={created.key} label="api key" w={{ base: 200, lg: 250 }} />
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text>Secret</Text>
|
||||
<CopyText content={created.secret} label="api secret" />
|
||||
<CopyText content={created.secret} label="api secret" w={{ base: 200, lg: 250 }} />
|
||||
</Group>
|
||||
|
||||
<Group justify="end" onClick={close}>
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useExecute, useSelectedResources, useWrite } from "@/lib/hooks";
|
||||
import { sendCopyNotification, usableResourceExecuteKey } from "@/lib/utils";
|
||||
import { UsableResource } from "@/resources";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import ConfirmButton from "@/ui/confirm-button";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Menu,
|
||||
Modal,
|
||||
Stack,
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { Types } from "komodo_client";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { FC, useState } from "react";
|
||||
|
||||
type Request = Types.ExecuteRequest["type"] | Types.WriteRequest["type"];
|
||||
|
||||
export interface BatchExecutionsProps<T extends Request> {
|
||||
type: UsableResource;
|
||||
executions: T[];
|
||||
executions: [T, FC<{ size?: string | number }>][];
|
||||
}
|
||||
|
||||
export default function BatchExecutions<T extends Request>({
|
||||
@@ -46,6 +46,7 @@ export default function BatchExecutions<T extends Request>({
|
||||
<BatchExecutionsModal
|
||||
type={type}
|
||||
execution={execution}
|
||||
icon={executions.find((e) => e[0] === execution)?.[1] ?? ICONS.Check}
|
||||
onClose={() => setExecution(undefined)}
|
||||
/>
|
||||
</>
|
||||
@@ -59,7 +60,7 @@ function BatchExecutionsDropdownMenu<T extends Request>({
|
||||
disabled,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
executions: T[];
|
||||
executions: [T, FC<{ size?: string | number }>][];
|
||||
onSelect: (item: T) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
@@ -90,9 +91,10 @@ function BatchExecutionsDropdownMenu<T extends Request>({
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{executions.map((execution) => (
|
||||
{executions.map(([execution, Icon]) => (
|
||||
<Menu.Item
|
||||
key={execution}
|
||||
leftSection={<Icon size="1rem" />}
|
||||
onClick={() => onSelect(execution)}
|
||||
renderRoot={(props) => <Button fullWidth {...props} />}
|
||||
>
|
||||
@@ -127,23 +129,29 @@ function BatchExecutionsDropdownMenu<T extends Request>({
|
||||
function BatchExecutionsModal({
|
||||
type,
|
||||
execution,
|
||||
onClose,
|
||||
icon: Icon,
|
||||
onClose: _onClose,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
execution: Request | undefined;
|
||||
icon: FC<{ size?: string | number }>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [selected, setSelected] = useSelectedResources(type);
|
||||
const [input, setInput] = useState("");
|
||||
const onClose = () => {
|
||||
setInput("");
|
||||
_onClose();
|
||||
};
|
||||
|
||||
const { mutate: execute, isPending: executePending } = useExecute(
|
||||
execution! as Types.ExecuteRequest["type"],
|
||||
execution as Types.ExecuteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
},
|
||||
);
|
||||
const { mutate: write, isPending: writePending } = useWrite(
|
||||
execution! as Types.WriteRequest["type"],
|
||||
execution as Types.WriteRequest["type"],
|
||||
{
|
||||
onSuccess: onClose,
|
||||
},
|
||||
@@ -200,8 +208,10 @@ function BatchExecutionsModal({
|
||||
)}
|
||||
|
||||
<Group justify="end">
|
||||
<ConfirmButton
|
||||
icon={<ICONS.Check size="1rem" />}
|
||||
<Button
|
||||
leftSection={
|
||||
isPending ? <Loader size="1rem" /> : <Icon size="1rem" />
|
||||
}
|
||||
onClick={() => {
|
||||
for (const resource of selected) {
|
||||
if (execution.startsWith("Delete")) {
|
||||
@@ -226,10 +236,9 @@ function BatchExecutionsModal({
|
||||
? false
|
||||
: input !== formatted
|
||||
}
|
||||
loading={isPending}
|
||||
>
|
||||
Confirm
|
||||
</ConfirmButton>
|
||||
{formatted}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRead, useSearchCombobox } from "@/lib/hooks";
|
||||
import { filterBySplit } from "@/lib/utils";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
ButtonProps,
|
||||
Combobox,
|
||||
@@ -13,6 +14,11 @@ import { ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { DOCKER_LINK_ICONS } from "./docker/link";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import {
|
||||
colorByIntention,
|
||||
containerStateIntention,
|
||||
swarmStateIntention,
|
||||
} from "@/lib/color";
|
||||
|
||||
export interface StackServiceSelectorProps extends ComboboxProps {
|
||||
stackId: string;
|
||||
@@ -22,6 +28,7 @@ export interface StackServiceSelectorProps extends ComboboxProps {
|
||||
placeholder?: string;
|
||||
state?: Types.ContainerStateStatusEnum;
|
||||
targetProps?: ButtonProps;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
export default function StackServiceSelector({
|
||||
@@ -34,22 +41,33 @@ export default function StackServiceSelector({
|
||||
position = "bottom-start",
|
||||
onOptionSubmit,
|
||||
targetProps,
|
||||
clearable = true,
|
||||
...comboboxProps
|
||||
}: StackServiceSelectorProps) {
|
||||
const services = useRead("ListStackServices", {
|
||||
stack: stackId,
|
||||
}).data?.filter((service) => !state || service?.container?.state === state);
|
||||
|
||||
const firstService = services?.[0].service;
|
||||
useEffect(() => {
|
||||
firstService && onSelect?.(firstService);
|
||||
}, [firstService]);
|
||||
const name = services?.find((s) => s.service === selected)?.service;
|
||||
const container = services?.find((s) => s.service === selected)?.container;
|
||||
|
||||
const selectedService = services?.find((s) => s.service === selected);
|
||||
const name = selectedService?.service;
|
||||
const container = selectedService?.container;
|
||||
const swarmService = selectedService?.swarm_service;
|
||||
|
||||
const intention = !selectedService
|
||||
? "None"
|
||||
: swarmService?.State
|
||||
? swarmStateIntention(swarmService.State)
|
||||
: containerStateIntention(
|
||||
container?.state ?? Types.ContainerStateStatusEnum.Empty,
|
||||
);
|
||||
|
||||
const { search, setSearch, combobox } = useSearchCombobox();
|
||||
|
||||
if (!services) return null;
|
||||
|
||||
const filtered = filterBySplit(services, search, (item) => item.service).sort(
|
||||
(a, b) => {
|
||||
if (a.service > b.service) {
|
||||
@@ -76,21 +94,38 @@ export default function StackServiceSelector({
|
||||
>
|
||||
<Combobox.Target>
|
||||
<Button
|
||||
maw={350}
|
||||
justify="space-between"
|
||||
disabled={disabled}
|
||||
rightSection={<ChevronsUpDown size="0.9rem" />}
|
||||
w="fit-content"
|
||||
maw="100%"
|
||||
rightSection={
|
||||
<Group gap="xs" ml="sm" wrap="nowrap">
|
||||
{clearable && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect?.("");
|
||||
}}
|
||||
disabled={disabled || !selected}
|
||||
>
|
||||
<ICONS.Clear size="0.8rem" />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ChevronsUpDown size="1rem" />
|
||||
</Group>
|
||||
}
|
||||
onClick={() => combobox.toggleDropdown()}
|
||||
disabled={!stackId || disabled}
|
||||
loading={!!stackId && !services}
|
||||
{...targetProps}
|
||||
>
|
||||
<Group gap="xs">
|
||||
{container && (
|
||||
<DOCKER_LINK_ICONS.Container
|
||||
serverId={container.server_id!}
|
||||
name={container.name}
|
||||
/>
|
||||
)}
|
||||
<Text>{name || (placeholder ?? "Select container")}</Text>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ICONS.Service size="1rem" color={colorByIntention(intention)} />
|
||||
<Text className="text-ellipsis">
|
||||
{name || (placeholder ?? "Select service")}
|
||||
</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
</Combobox.Target>
|
||||
|
||||
@@ -127,7 +127,9 @@ export function useAuthState() {
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
const _passkey = search.get("passkey");
|
||||
const passkey = _passkey ? JSON.parse(_passkey) : null;
|
||||
const passkey = _passkey
|
||||
? JSON.parse(MoghAuth.Passkey.base64UrlDecode(_passkey))
|
||||
: null;
|
||||
|
||||
// guard against multiple reqs sent
|
||||
// maybe isPending would do this but not sure about with render loop, this for sure will.
|
||||
|
||||
@@ -343,3 +343,44 @@ export function listsEqual(a: string[], b: string[]) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does deep compare of 2 items, returning `true` if equal.
|
||||
*
|
||||
* - Functions: Always `true`
|
||||
* - Primitives: Returns direct `a === b`
|
||||
* - Arrays: Returns same items and ordering (recursive)
|
||||
* - Objects: Returns same keys / values (recursive)
|
||||
*
|
||||
* @param a Item a
|
||||
* @param b Item b
|
||||
* @returns a === b
|
||||
*/
|
||||
export function deepCompare(a: any, b: any) {
|
||||
const ta = typeof a;
|
||||
const tb = typeof b;
|
||||
|
||||
if (ta !== tb) return false;
|
||||
|
||||
if (ta === "function") return true;
|
||||
|
||||
if (ta === "object") {
|
||||
const ea = Object.entries(a);
|
||||
const kb = Object.keys(b);
|
||||
|
||||
// Length not equal -> false
|
||||
if (ea.length !== kb.length) return false;
|
||||
|
||||
for (const [key, va] of ea) {
|
||||
const vb = b[key];
|
||||
|
||||
// Early return when any not equal
|
||||
if (!deepCompare(va, vb)) return false;
|
||||
}
|
||||
|
||||
// If it gets through all, it's equal
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ export default function ContainerTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -24,9 +24,10 @@ export default function Settings() {
|
||||
<Stack gap="xl" mb="50vh">
|
||||
<SettingsCoreInfo />
|
||||
<MobileFriendlyTabs
|
||||
changeAt="xl"
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
actions={currentView === "Variables" && <ExportToml includeVariables />}
|
||||
tabsProps={{ color: "green" }}
|
||||
tabs={[
|
||||
{
|
||||
value: "Variables",
|
||||
@@ -65,9 +66,6 @@ export default function Settings() {
|
||||
icon: ICONS.OnboardingKey,
|
||||
},
|
||||
]}
|
||||
actions={currentView === "Variables" && <ExportToml includeVariables />}
|
||||
tabProps={{ w: 150 }}
|
||||
tabsProps={{ color: "green" }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
useMatches,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
@@ -55,6 +56,8 @@ export default function NewOnboardingKey() {
|
||||
_close();
|
||||
};
|
||||
|
||||
const size = useMatches({ base: "90%", md: 500 });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -66,7 +69,7 @@ export default function NewOnboardingKey() {
|
||||
{created && "Onboarding Key Created"}
|
||||
</Text>
|
||||
}
|
||||
size="lg"
|
||||
size={size}
|
||||
>
|
||||
<Stack>
|
||||
{!created && (
|
||||
@@ -117,11 +120,15 @@ export default function NewOnboardingKey() {
|
||||
|
||||
{created && (
|
||||
<>
|
||||
<Text>
|
||||
Use as the <b>PERIPHERY_ONBOARDING_KEY</b>
|
||||
<Text size="md" my="sm">
|
||||
Copy the onboarding key below. <b>It won't be shown again</b>.
|
||||
</Text>
|
||||
|
||||
<CopyText content={created.private_key} label="private key" />
|
||||
<CopyText
|
||||
content={created.private_key}
|
||||
label="private key"
|
||||
w="90%"
|
||||
/>
|
||||
|
||||
<Group justify="end" onClick={close}>
|
||||
<Button leftSection={<ICONS.Clear />}>Close</Button>
|
||||
|
||||
@@ -93,7 +93,6 @@ export default function StackServiceTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ export default function SwarmConfigEditSection({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: data }}
|
||||
content={{ contents: edit }}
|
||||
original={{ contents: data }}
|
||||
update={{ contents: edit }}
|
||||
onConfirm={async () =>
|
||||
name &&
|
||||
edit !== undefined &&
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function SwarmConfigTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ export default function SwarmNodeTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ export default function SwarmSecretEditSection({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: "" }}
|
||||
content={{ contents: edit }}
|
||||
original={{ contents: "" }}
|
||||
update={{ contents: edit }}
|
||||
onConfirm={async () =>
|
||||
name &&
|
||||
edit !== undefined &&
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function SwarmSecretTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ export default function SwarmServiceTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ export default function SwarmStackTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ export default function SwarmTaskTabs({
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ActionComponents: RequiredResourceComponents<
|
||||
New: () => <NewResource type="Action" />,
|
||||
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions type="Action" executions={["RunAction"]} />
|
||||
<BatchExecutions type="Action" executions={[["RunAction", ICONS.Run]]} />
|
||||
),
|
||||
|
||||
Table: ActionTable,
|
||||
@@ -158,7 +158,7 @@ export const ActionComponents: RequiredResourceComponents<
|
||||
|
||||
return (
|
||||
<ConfirmModalWithDisable
|
||||
icon={<ICONS.Action size="1rem" />}
|
||||
icon={<ICONS.Run size="1rem" />}
|
||||
confirmText={action.name}
|
||||
onConfirm={async () => {
|
||||
await mutateAsync({ action: id, args: {} });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRead } from "@/lib/hooks";
|
||||
import { useExecute, useRead } from "@/lib/hooks";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import { RequiredResourceComponents } from "..";
|
||||
import { Types } from "komodo_client";
|
||||
@@ -8,6 +8,7 @@ import ResourceHeader from "../header";
|
||||
import AlerterConfig from "./config";
|
||||
import { hexColorByIntention } from "@/lib/color";
|
||||
import BatchExecutions from "@/components/batch-executions";
|
||||
import ConfirmButton from "@/ui/confirm-button";
|
||||
|
||||
export function useAlerter(id: string | undefined) {
|
||||
return useRead("ListAlerters", {}).data?.find((r) => r.id === id);
|
||||
@@ -38,7 +39,10 @@ export const AlerterComponents: RequiredResourceComponents<
|
||||
New: () => <NewResource type="Alerter" />,
|
||||
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions type="Alerter" executions={["TestAlerter"]} />
|
||||
<BatchExecutions
|
||||
type="Alerter"
|
||||
executions={[["TestAlerter", ICONS.Test]]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: AlerterTable,
|
||||
@@ -72,7 +76,23 @@ export const AlerterComponents: RequiredResourceComponents<
|
||||
State: () => null,
|
||||
Info: {},
|
||||
|
||||
Executions: {},
|
||||
Executions: {
|
||||
TestAlerter: ({ id }) => {
|
||||
const { mutate, isPending } = useExecute("TestAlerter");
|
||||
const alerter = useAlerter(id);
|
||||
if (!alerter) return null;
|
||||
return (
|
||||
<ConfirmButton
|
||||
icon={<ICONS.Test size="1rem" />}
|
||||
loading={isPending}
|
||||
onClick={() => mutate({ alerter: id })}
|
||||
disabled={isPending}
|
||||
>
|
||||
Test Alerter
|
||||
</ConfirmButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Config: AlerterConfig,
|
||||
|
||||
|
||||
@@ -87,7 +87,13 @@ export const BuildComponents: RequiredResourceComponents<
|
||||
},
|
||||
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions type="Build" executions={["RunBuild"]} />
|
||||
<BatchExecutions
|
||||
type="Build"
|
||||
executions={[
|
||||
["RunBuild", ICONS.Build],
|
||||
["CancelBuild", ICONS.Cancel],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: BuildTable,
|
||||
|
||||
@@ -152,8 +152,8 @@ export default function BuildInfo({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: remoteContents }}
|
||||
content={{ contents: edits.contents }}
|
||||
original={{ contents: remoteContents }}
|
||||
update={{ contents: edits.contents }}
|
||||
onConfirm={async () => {
|
||||
if (build) {
|
||||
return await mutateAsync({
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function DeploymentNetworkSelector({
|
||||
}}
|
||||
disabled={disabled}
|
||||
data={networks}
|
||||
w="fit-content"
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fmtUpperCamelcase } from "@/lib/formatting";
|
||||
import { ConfigItem } from "@/ui/config/item";
|
||||
import { Select } from "@mantine/core";
|
||||
import { Types } from "komodo_client";
|
||||
@@ -27,13 +28,11 @@ export default function DeploymentRestartSelector({
|
||||
disabled={disabled}
|
||||
placeholder="Select Mode"
|
||||
data={Object.entries(Types.RestartMode).map(([label, value]) => ({
|
||||
label:
|
||||
label === "NoRestart"
|
||||
? "don't restart"
|
||||
: value.split("-").join(" "),
|
||||
label: fmtUpperCamelcase(label),
|
||||
value,
|
||||
}))}
|
||||
w="fit-content"
|
||||
tt="capitalize"
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ export function TerminationSignal({
|
||||
disabled={disabled}
|
||||
placeholder="Select signal"
|
||||
data={Object.values(Types.TerminationSignal).reverse()}
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
@@ -90,12 +90,12 @@ export const DeploymentComponents: RequiredResourceComponents<
|
||||
<BatchExecutions
|
||||
type="Deployment"
|
||||
executions={[
|
||||
"CheckDeploymentForUpdate",
|
||||
"PullDeployment",
|
||||
"Deploy",
|
||||
"RestartDeployment",
|
||||
"StopDeployment",
|
||||
"DestroyDeployment",
|
||||
["CheckDeploymentForUpdate", ICONS.UpdateAvailable],
|
||||
["PullDeployment", ICONS.Pull],
|
||||
["Deploy", ICONS.Deploy],
|
||||
["RestartDeployment", ICONS.Restart],
|
||||
["StopDeployment", ICONS.Stop],
|
||||
["DestroyDeployment", ICONS.Destroy],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import EntityHeader, { EntityHeaderProps } from "@/ui/entity-header";
|
||||
import { UsableResource } from ".";
|
||||
import { useWrite } from "@/lib/hooks";
|
||||
import { useInvalidate, useWrite } from "@/lib/hooks";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import ResourceHeaderAction from "./header-action";
|
||||
import { Types } from "komodo_client";
|
||||
@@ -17,10 +17,12 @@ export default function ResourceHeader({
|
||||
resource,
|
||||
...props
|
||||
}: ResourceHeaderProps) {
|
||||
const inv = useInvalidate();
|
||||
const { mutateAsync: rename, isPending: renamePending } = useWrite(
|
||||
`Rename${type}`,
|
||||
{
|
||||
onSuccess: () => {
|
||||
inv([`List${type}s`], [`Get${type}`]);
|
||||
notifications.show({ message: "Renamed " + type, color: "green" });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { Types } from "komodo_client";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
@@ -115,6 +116,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
setParams({ action: params.action, args: JSON.parse(args) })
|
||||
}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="json"
|
||||
/>
|
||||
</Group>
|
||||
@@ -131,6 +133,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -158,6 +161,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -198,6 +202,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -290,6 +295,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -317,6 +323,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -343,6 +350,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -369,6 +377,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -450,6 +459,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -470,7 +480,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
pull: undefined,
|
||||
},
|
||||
Component: ({ params, setParams, disabled }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure();
|
||||
// local mirrors to allow cancel without committing
|
||||
const [stack, setStack] = useState(params.stack ?? "");
|
||||
const [service, setService] = useState(params.service ?? "");
|
||||
@@ -551,128 +561,167 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
detach: detach ? true : undefined,
|
||||
env,
|
||||
} as any);
|
||||
setOpen(false);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button disabled={disabled}>Configure</Button>
|
||||
<Button disabled={disabled} onClick={open}>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title="Run Stack Service"
|
||||
size="lg"
|
||||
>
|
||||
<Stack>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<Group>
|
||||
<Text c="dimmed">Stack</Text>
|
||||
<Stack gap="lg">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }}>
|
||||
<Stack gap="0">
|
||||
<Text>Stack</Text>
|
||||
<ResourceSelector
|
||||
type="Stack"
|
||||
selected={stack}
|
||||
onSelect={(id) => setStack(id)}
|
||||
disabled={disabled}
|
||||
width="target"
|
||||
targetProps={{ w: "100%" }}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text c="dimmed">Service</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="0">
|
||||
<Text>Service</Text>
|
||||
<StackServiceSelector
|
||||
stackId={stack}
|
||||
selected={service}
|
||||
onSelect={setService}
|
||||
disabled={disabled}
|
||||
width="target"
|
||||
targetProps={{ w: "100%" }}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
|
||||
<Group>
|
||||
<Text c="dimmed">Command</Text>
|
||||
<Stack gap="0">
|
||||
<Text>Command</Text>
|
||||
<TextInput
|
||||
placeholder="Enter command (Required)"
|
||||
value={commandText}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<SimpleGrid cols={{ base: 2, md: 4 }}>
|
||||
<EnableSwitch
|
||||
label="No TTY"
|
||||
checked={no_tty}
|
||||
onCheckedChange={setNoTty}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="No Dependencies"
|
||||
checked={no_deps}
|
||||
onCheckedChange={setNoDeps}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Detach"
|
||||
checked={detach}
|
||||
onCheckedChange={setDetach}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Service Ports"
|
||||
checked={service_ports}
|
||||
onCheckedChange={setServicePorts}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Pull Image"
|
||||
checked={pull}
|
||||
onCheckedChange={setPull}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }}>
|
||||
<Stack gap="0">
|
||||
<Text>Working Directory</Text>
|
||||
<TextInput
|
||||
placeholder="/work/dir (Optional)"
|
||||
value={workdir}
|
||||
onChange={(e) => setWorkdir(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap="0">
|
||||
<Text>User</Text>
|
||||
<TextInput
|
||||
placeholder="uid:gid or user (Optional)"
|
||||
value={user}
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<Group>
|
||||
<Text c="dimmed">Working Directory</Text>
|
||||
<TextInput
|
||||
placeholder="/work/dir"
|
||||
value={workdir}
|
||||
onChange={(e) => setWorkdir(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text c="dimmed">User</Text>
|
||||
<TextInput
|
||||
placeholder="uid:gid or user"
|
||||
value={user}
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text c="dimmed">Entrypoint</Text>
|
||||
<Stack gap="0">
|
||||
<Text>Entrypoint</Text>
|
||||
<TextInput
|
||||
placeholder="Custom entrypoint (Optional)"
|
||||
value={entrypoint}
|
||||
onChange={(e) => setEntrypoint(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Group>
|
||||
<Text c="dimmed">Extra Environment Variables</Text>
|
||||
<MonacoEditor
|
||||
value={envText}
|
||||
onValueChange={setEnvText}
|
||||
language="key_value"
|
||||
readOnly={disabled}
|
||||
/>
|
||||
</Group>
|
||||
<Stack gap="0">
|
||||
<Text>Extra Env</Text>
|
||||
<MonacoEditor
|
||||
value={envText}
|
||||
onValueChange={setEnvText}
|
||||
language="key_value"
|
||||
readOnly={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{!disabled && (
|
||||
<Button onClick={onConfirm} leftSection={<CheckCircle />}>
|
||||
Confirm
|
||||
</Button>
|
||||
)}
|
||||
<Stack gap="0">
|
||||
<Text>Options</Text>
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, sm: 2 }}
|
||||
className="accent-hover-light"
|
||||
p="md"
|
||||
bdrs="md"
|
||||
style={{ placeItems: "center" }}
|
||||
>
|
||||
<EnableSwitch
|
||||
label="No TTY"
|
||||
checked={no_tty}
|
||||
onCheckedChange={setNoTty}
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
w: 210,
|
||||
justify: "end",
|
||||
}}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="No Dependencies"
|
||||
checked={no_deps}
|
||||
onCheckedChange={setNoDeps}
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
w: 210,
|
||||
justify: "end",
|
||||
}}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Detach"
|
||||
checked={detach}
|
||||
onCheckedChange={setDetach}
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
w: 210,
|
||||
justify: "end",
|
||||
}}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Service Ports"
|
||||
checked={service_ports}
|
||||
onCheckedChange={setServicePorts}
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
w: 210,
|
||||
justify: "end",
|
||||
}}
|
||||
/>
|
||||
<EnableSwitch
|
||||
label="Pull Image"
|
||||
checked={pull}
|
||||
onCheckedChange={setPull}
|
||||
disabled={disabled}
|
||||
labelProps={{
|
||||
w: 210,
|
||||
justify: "end",
|
||||
}}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
{!disabled && (
|
||||
<Button onClick={onConfirm} leftSection={<CheckCircle />}>
|
||||
Confirm
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
@@ -701,6 +750,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -727,6 +777,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -753,6 +804,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
}
|
||||
onUpdate={(pattern) => setParams({ pattern })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage="string_list"
|
||||
/>
|
||||
),
|
||||
@@ -1042,6 +1094,7 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = {
|
||||
placeholder="Configure custom alert message"
|
||||
onUpdate={(message) => setParams({ message })}
|
||||
disabled={disabled}
|
||||
useMonaco
|
||||
monacoLanguage={undefined}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -15,7 +15,7 @@ export function RunProcedure({ id }: { id: string }) {
|
||||
return (
|
||||
<ConfirmModalWithDisable
|
||||
confirmText={procedure.name}
|
||||
icon={<ICONS.Start size="1rem" />}
|
||||
icon={<ICONS.Run size="1rem" />}
|
||||
onConfirm={() => run({ procedure: id })}
|
||||
disabled={running || isPending}
|
||||
loading={running || isPending}
|
||||
|
||||
@@ -10,6 +10,10 @@ import ProcedureConfig from "./config";
|
||||
import { RunProcedure } from "./executions";
|
||||
import ResourceHeader from "../header";
|
||||
import BatchExecutions from "@/components/batch-executions";
|
||||
import { Badge, Group, Popover, Text } from "@mantine/core";
|
||||
import { Clock } from "lucide-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { updateLogToHtml } from "@/lib/utils";
|
||||
|
||||
export function useProcedure(id: string | undefined) {
|
||||
return useRead("ListProcedures", {}).data?.find((r) => r.id === id);
|
||||
@@ -57,7 +61,10 @@ export const ProcedureComponents: RequiredResourceComponents<
|
||||
New: () => <NewResource type="Procedure" />,
|
||||
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions type="Procedure" executions={["RunProcedure"]} />
|
||||
<BatchExecutions
|
||||
type="Procedure"
|
||||
executions={[["RunProcedure", ICONS.Run]]}
|
||||
/>
|
||||
),
|
||||
|
||||
Table: ProcedureTable,
|
||||
@@ -91,7 +98,50 @@ export const ProcedureComponents: RequiredResourceComponents<
|
||||
let state = useProcedure(id)?.info.state;
|
||||
return <StatusBadge text={state} intent={procedureStateIntention(state)} />;
|
||||
},
|
||||
Info: {},
|
||||
Info: {
|
||||
Schedule: ({ id }) => {
|
||||
const nextScheduledRun = useProcedure(id)?.info.next_scheduled_run;
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<Clock size="1rem" />
|
||||
Next Run:
|
||||
<Text fw="bold">
|
||||
{nextScheduledRun
|
||||
? new Date(nextScheduledRun).toLocaleString()
|
||||
: "Not Scheduled"}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
ScheduleErrors: ({ id }) => {
|
||||
const [opened, { close, open }] = useDisclosure(false);
|
||||
const error = useProcedure(id)?.info.schedule_error;
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover position="bottom-start" opened={opened}>
|
||||
<Popover.Target>
|
||||
<Badge color="red" onMouseEnter={open} onMouseLeave={close}>
|
||||
Schedule Error
|
||||
</Badge>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown style={{ pointerEvents: "none" }}>
|
||||
<Text
|
||||
component="pre"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateLogToHtml(error),
|
||||
}}
|
||||
fz="xs"
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Executions: {
|
||||
RunProcedure,
|
||||
|
||||
@@ -303,11 +303,14 @@ export default function RepoConfig({
|
||||
},
|
||||
{
|
||||
label: "Links",
|
||||
description: "Add quick links in the resource header",
|
||||
contentHidden: ((update.links ?? config.links)?.length ?? 0) === 0,
|
||||
labelHidden: true,
|
||||
fields: {
|
||||
links: (values, set) => (
|
||||
<ConfigList
|
||||
label="Links"
|
||||
boldLabel
|
||||
addLabel="Add Link"
|
||||
description="Add quick links in the resource header"
|
||||
field="links"
|
||||
values={values ?? []}
|
||||
set={set}
|
||||
|
||||
124
ui/src/resources/repo/executions.tsx
Normal file
124
ui/src/resources/repo/executions.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useExecute, usePermissions, useRead } from "@/lib/hooks";
|
||||
import { useRepo } from ".";
|
||||
import { useBuilder } from "../builder";
|
||||
import { Types } from "komodo_client";
|
||||
import ConfirmButton from "@/ui/confirm-button";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
|
||||
export function CloneRepo({ id }: { id: string }) {
|
||||
const { mutate, isPending } = useExecute("CloneRepo");
|
||||
const cloning = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
{ refetchInterval: 5000 },
|
||||
).data?.cloning;
|
||||
const info = useRepo(id)?.info;
|
||||
if (!info?.server_id) return null;
|
||||
const hash = info?.latest_hash;
|
||||
const isCloned = (hash?.length || 0) > 0;
|
||||
const pending = isPending || cloning;
|
||||
return (
|
||||
<ConfirmButton
|
||||
icon={<ICONS.CloneRepo size="1rem" />}
|
||||
onClick={() => mutate({ repo: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
>
|
||||
{isCloned ? "Reclone" : "Clone"}
|
||||
</ConfirmButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function PullRepo({ id }: { id: string }) {
|
||||
const { mutate, isPending } = useExecute("PullRepo");
|
||||
const pulling = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
{ refetchInterval: 5000 },
|
||||
).data?.pulling;
|
||||
const info = useRepo(id)?.info;
|
||||
if (!info?.server_id) return null;
|
||||
const hash = info?.latest_hash;
|
||||
const isCloned = (hash?.length || 0) > 0;
|
||||
if (!isCloned) return null;
|
||||
const pending = isPending || pulling;
|
||||
return (
|
||||
<ConfirmButton
|
||||
icon={<ICONS.PullRepo size="1rem" />}
|
||||
onClick={() => mutate({ repo: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
>
|
||||
Pull
|
||||
</ConfirmButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function BuildRepo({ id }: { id: string }) {
|
||||
const { canExecute } = usePermissions({ type: "Repo", id });
|
||||
const building = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
{ refetchInterval: 5000 },
|
||||
).data?.building;
|
||||
const updates = useRead("ListUpdates", {
|
||||
query: {
|
||||
"target.type": "Repo",
|
||||
"target.id": id,
|
||||
},
|
||||
}).data;
|
||||
const { mutate: run_mutate, isPending: runPending } = useExecute("BuildRepo");
|
||||
const { mutate: cancel_mutate, isPending: cancelPending } =
|
||||
useExecute("CancelRepoBuild");
|
||||
|
||||
const repo = useRepo(id);
|
||||
const builder = useBuilder(repo?.info.builder_id);
|
||||
const canCancel = builder?.info.builder_type !== "Server";
|
||||
|
||||
// Don't show if builder not attached
|
||||
if (!builder) return null;
|
||||
|
||||
// make sure hidden without perms.
|
||||
// not usually necessary, but this button also used in deployment actions.
|
||||
if (!canExecute) return null;
|
||||
|
||||
// updates come in in descending order, so 'find' will find latest update matching operation
|
||||
const latestBuild = updates?.updates.find(
|
||||
(u) => u.operation === Types.Operation.BuildRepo,
|
||||
);
|
||||
const latestCancel = updates?.updates.find(
|
||||
(u) => u.operation === Types.Operation.CancelRepoBuild,
|
||||
);
|
||||
const cancelDisabled =
|
||||
!canCancel ||
|
||||
cancelPending ||
|
||||
(latestCancel && latestBuild
|
||||
? latestCancel!.start_ts > latestBuild!.start_ts
|
||||
: false);
|
||||
|
||||
if (building) {
|
||||
return (
|
||||
<ConfirmButton
|
||||
variant="filled"
|
||||
color="red"
|
||||
icon={<ICONS.Cancel size="1rem" />}
|
||||
onClick={() => cancel_mutate({ repo: id })}
|
||||
disabled={cancelDisabled}
|
||||
loading={cancelPending}
|
||||
>
|
||||
Cancel Build
|
||||
</ConfirmButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ConfirmButton
|
||||
icon={<ICONS.Build size="1rem" />}
|
||||
onClick={() => run_mutate({ repo: id })}
|
||||
disabled={runPending || building}
|
||||
loading={runPending || building}
|
||||
>
|
||||
Build
|
||||
</ConfirmButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,12 @@ import NewResource from "@/resources/new";
|
||||
import ResourceHeader from "@/resources/header";
|
||||
import RepoTabs from "./tabs";
|
||||
import BatchExecutions from "@/components/batch-executions";
|
||||
import { BuildRepo, CloneRepo, PullRepo } from "./executions";
|
||||
import { useServer } from "../server";
|
||||
import { useBuilder } from "../builder";
|
||||
import { Box, Group } from "@mantine/core";
|
||||
import ResourceLink from "../link";
|
||||
import RepoLink from "@/components/repo-link";
|
||||
|
||||
export function useRepo(id: string | undefined) {
|
||||
return useRead("ListRepos", {}).data?.find((r) => r.id === id);
|
||||
@@ -58,7 +64,11 @@ export const RepoComponents: RequiredResourceComponents<
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions
|
||||
type="Repo"
|
||||
executions={["PullRepo", "CloneRepo", "BuildRepo"]}
|
||||
executions={[
|
||||
["PullRepo", ICONS.PullRepo],
|
||||
["CloneRepo", ICONS.CloneRepo],
|
||||
["BuildRepo", ICONS.Build],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -92,9 +102,52 @@ export const RepoComponents: RequiredResourceComponents<
|
||||
let state = useRepo(id)?.info.state;
|
||||
return <StatusBadge text={state} intent={repoStateIntention(state)} />;
|
||||
},
|
||||
Info: {},
|
||||
Info: {
|
||||
Target: ({ id }) => {
|
||||
const info = useRepo(id)?.info;
|
||||
const server = useServer(info?.server_id);
|
||||
const builder = useBuilder(info?.builder_id);
|
||||
|
||||
Executions: {},
|
||||
if (!info?.server_id && !info?.builder_id) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{server?.id && (
|
||||
<Box>
|
||||
<ResourceLink type="Server" id={server.id} />
|
||||
</Box>
|
||||
)}
|
||||
{builder?.id && (
|
||||
<Box>
|
||||
<ResourceLink type="Builder" id={builder.id} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
Source: ({ id }) => {
|
||||
const info = useRepo(id)?.info;
|
||||
|
||||
if (!info?.repo || !info?.repo_link) return null;
|
||||
|
||||
return <RepoLink repo={info?.repo} link={info?.repo_link} />;
|
||||
},
|
||||
Branch: ({ id }) => {
|
||||
const branch = useRepo(id)?.info.branch;
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ICONS.Branch size="1rem" />
|
||||
{branch}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Executions: {
|
||||
CloneRepo,
|
||||
PullRepo,
|
||||
BuildRepo,
|
||||
},
|
||||
|
||||
Config: RepoTabs,
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ export default function ServerDockerResources({
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -70,13 +70,13 @@ export const ServerComponents: RequiredResourceComponents<
|
||||
<BatchExecutions
|
||||
type="Server"
|
||||
executions={[
|
||||
"PruneContainers",
|
||||
"PruneNetworks",
|
||||
"PruneVolumes",
|
||||
"PruneImages",
|
||||
"PruneSystem",
|
||||
"RestartAllContainers",
|
||||
"StopAllContainers",
|
||||
["PruneContainers", ICONS.Container],
|
||||
["PruneNetworks", ICONS.Network],
|
||||
["PruneVolumes", ICONS.Volume],
|
||||
["PruneImages", ICONS.Image],
|
||||
["PruneSystem", ICONS.System],
|
||||
["RestartAllContainers", ICONS.Restart],
|
||||
["StopAllContainers", ICONS.Stop],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -96,12 +96,12 @@ export const StackComponents: RequiredResourceComponents<
|
||||
<BatchExecutions
|
||||
type="Stack"
|
||||
executions={[
|
||||
"CheckStackForUpdate",
|
||||
"PullStack",
|
||||
"DeployStack",
|
||||
"RestartStack",
|
||||
"StopStack",
|
||||
"DestroyStack",
|
||||
["CheckStackForUpdate", ICONS.UpdateAvailable],
|
||||
["PullStack", ICONS.Pull],
|
||||
["DeployStack", ICONS.Deploy],
|
||||
["RestartStack", ICONS.Restart],
|
||||
["StopStack", ICONS.Stop],
|
||||
["DestroyStack", ICONS.Destroy],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function StackInfo({
|
||||
const defaultShowContents = !latestContents || latestContents.length < 3;
|
||||
|
||||
return (
|
||||
<Section gap="xl" titleOther={titleOther}>
|
||||
<Section titleOther={titleOther}>
|
||||
{/* Errors */}
|
||||
{latestErrors &&
|
||||
latestErrors.length > 0 &&
|
||||
@@ -158,8 +158,8 @@ export default function StackInfo({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[content.path] }}
|
||||
original={{ contents: content.contents }}
|
||||
update={{ contents: edits[content.path] }}
|
||||
onConfirm={async () => {
|
||||
if (stack) {
|
||||
return await mutateAsync({
|
||||
|
||||
@@ -92,7 +92,6 @@ export default function SwarmDockerResources({
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabProps={{ w: 140 }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import ConfirmButton from "@/ui/confirm-button";
|
||||
import { useFullResourceSync } from ".";
|
||||
import { useResourceSyncTabsView } from "./hooks";
|
||||
import { fileContentsEmpty, resourceSyncNoChanges } from "@/lib/utils";
|
||||
import { NotebookPen, SquarePlay } from "lucide-react";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
|
||||
export function RefreshSync({ id }: { id: string }) {
|
||||
@@ -73,7 +72,7 @@ export function ExecuteSync({ id }: { id: string }) {
|
||||
return (
|
||||
<ConfirmModalWithDisable
|
||||
confirmText={sync.name}
|
||||
icon={<SquarePlay className="w-4 h-4" />}
|
||||
icon={<ICONS.Run size="1rem" />}
|
||||
onConfirm={() => execute({ sync: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
@@ -106,7 +105,7 @@ export function CommitSync({ id }: { id: string }) {
|
||||
if (freshSync) {
|
||||
return (
|
||||
<ConfirmButton
|
||||
icon={<NotebookPen className="w-4 h-4" />}
|
||||
icon={<ICONS.Commit size="1rem" />}
|
||||
onClick={() => commit({ sync: id })}
|
||||
disabled={isPending}
|
||||
loading={isPending}
|
||||
@@ -118,7 +117,7 @@ export function CommitSync({ id }: { id: string }) {
|
||||
return (
|
||||
<ConfirmModalWithDisable
|
||||
confirmText={sync.name}
|
||||
icon={<NotebookPen className="w-4 h-4" />}
|
||||
icon={<ICONS.Commit size="1rem" />}
|
||||
onConfirm={() => commit({ sync: id })}
|
||||
disabled={isPending}
|
||||
loading={isPending}
|
||||
|
||||
@@ -69,7 +69,10 @@ export const ResourceSyncComponents: RequiredResourceComponents<
|
||||
BatchExecutions: () => (
|
||||
<BatchExecutions
|
||||
type="ResourceSync"
|
||||
executions={["RunSync", "CommitSync"]}
|
||||
executions={[
|
||||
["RunSync", ICONS.Run],
|
||||
["CommitSync", ICONS.Commit],
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -142,7 +145,7 @@ export const ResourceSyncComponents: RequiredResourceComponents<
|
||||
|
||||
Executions: {
|
||||
RefreshSync,
|
||||
ExecuteSync,
|
||||
ExecuteSync: ExecuteSync,
|
||||
CommitSync,
|
||||
},
|
||||
|
||||
|
||||
@@ -170,8 +170,8 @@ export default function ResourceSyncInfo({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[keyPath] }}
|
||||
original={{ contents: content.contents }}
|
||||
update={{ contents: edits[keyPath] }}
|
||||
onConfirm={async () => {
|
||||
if (sync) {
|
||||
return await writeContents({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
AlarmClock,
|
||||
ArrowDownToDot,
|
||||
ArrowDownToLine,
|
||||
Ban,
|
||||
Bell,
|
||||
Box,
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
Factory,
|
||||
FileDown,
|
||||
FileText,
|
||||
FlaskConical,
|
||||
FolderCode,
|
||||
FolderGit,
|
||||
FolderSync,
|
||||
@@ -44,6 +47,7 @@ import {
|
||||
MemoryStick,
|
||||
Milestone,
|
||||
Network,
|
||||
NotebookPen,
|
||||
Package,
|
||||
Pause,
|
||||
Play,
|
||||
@@ -59,6 +63,7 @@ import {
|
||||
Server,
|
||||
Settings,
|
||||
Square,
|
||||
SquarePlay,
|
||||
SquareStack,
|
||||
Table,
|
||||
Tag,
|
||||
@@ -132,6 +137,12 @@ export const ICONS = {
|
||||
Pause,
|
||||
Prune: Scissors,
|
||||
Refresh: RefreshCcw,
|
||||
Run: SquarePlay,
|
||||
Test: FlaskConical,
|
||||
Cancel: Ban,
|
||||
CloneRepo: ArrowDownToLine,
|
||||
PullRepo: ArrowDownToDot,
|
||||
Commit: NotebookPen,
|
||||
// MISC
|
||||
User,
|
||||
Users,
|
||||
@@ -167,7 +178,6 @@ export const ICONS = {
|
||||
Download,
|
||||
Info,
|
||||
Edit,
|
||||
Cancel: Ban,
|
||||
NotFound: SearchX,
|
||||
Unknown: CircleQuestionMark,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Select,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
virtualColor,
|
||||
} from "@mantine/core";
|
||||
import { Types } from "komodo_client";
|
||||
@@ -253,6 +254,14 @@ const theme = createTheme({
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Tabs: Tabs.extend({
|
||||
styles: (theme) => ({
|
||||
list: {
|
||||
backgroundColor: theme.colors.accent[1],
|
||||
borderRadius: theme.radius.sm,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ import { Box, Button, Group, Modal, Stack, Text } from "@mantine/core";
|
||||
import { useCtrlKeyListener, useKeyListener } from "@/lib/hooks";
|
||||
import { fmtSnakeCaseToUpperSpaceCase } from "@/lib/formatting";
|
||||
import { ICONS } from "@/theme/icons";
|
||||
import { envToText } from "@/lib/utils";
|
||||
import { deepCompare, envToText } from "@/lib/utils";
|
||||
import { colorByIntention } from "@/lib/color";
|
||||
import ShowHideButton from "@/ui/show-hide-button";
|
||||
import ConfirmButton from "@/ui/confirm-button";
|
||||
|
||||
export default function ConfirmUpdate<T>({
|
||||
previous,
|
||||
content,
|
||||
original,
|
||||
update,
|
||||
onConfirm,
|
||||
loading,
|
||||
disabled,
|
||||
@@ -22,8 +21,8 @@ export default function ConfirmUpdate<T>({
|
||||
openKeyListener = true,
|
||||
confirmKeyListener = true,
|
||||
}: {
|
||||
previous: T;
|
||||
content: Partial<T>;
|
||||
original: T;
|
||||
update: Partial<T>;
|
||||
onConfirm: () => Promise<unknown>;
|
||||
loading?: boolean;
|
||||
disabled: boolean;
|
||||
@@ -56,28 +55,54 @@ export default function ConfirmUpdate<T>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title="Confirm Update" opened={opened} onClose={close} size="auto">
|
||||
<Stack gap="xl" w={1400} maw="95vw" my="lg">
|
||||
<Stack>
|
||||
{Object.entries(content).map(([key, val], i) => (
|
||||
<ConfirmUpdateItem
|
||||
key={i}
|
||||
_key={key as any}
|
||||
val={val as any}
|
||||
previous={previous}
|
||||
language={language}
|
||||
fileContentsLanguage={fileContentsLanguage}
|
||||
/>
|
||||
))}
|
||||
<Modal
|
||||
title={<Text size="xl">Confirm Update</Text>}
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
size="auto"
|
||||
styles={{ content: { overflowY: "hidden" } }}
|
||||
>
|
||||
<Stack
|
||||
gap="xl"
|
||||
w={1400}
|
||||
maw={{
|
||||
base: "calc(100vw - 100px)",
|
||||
xs: "calc(100vw - 150px)",
|
||||
sm: "calc(100vw - 200px)",
|
||||
md: "calc(100vw - 250px)",
|
||||
}}
|
||||
my="lg"
|
||||
style={{ overflowY: "hidden" }}
|
||||
>
|
||||
<Stack
|
||||
mah="min(calc(100vh - 300px), 800px)"
|
||||
style={{ overflowY: "auto" }}
|
||||
>
|
||||
{Object.entries(update)
|
||||
.filter(([key, val]) => !deepCompare((original as any)[key], val))
|
||||
.map(([key, val], i) => (
|
||||
<ConfirmUpdateItem
|
||||
key={i}
|
||||
_key={key as any}
|
||||
val={val as any}
|
||||
previous={original}
|
||||
language={language}
|
||||
fileContentsLanguage={fileContentsLanguage}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Group justify="flex-end">
|
||||
<ConfirmButton
|
||||
icon={<ICONS.Save size="1rem" />}
|
||||
onClick={handleConfirm}
|
||||
<Button
|
||||
leftSection={<ICONS.Save size="1rem" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConfirm();
|
||||
}}
|
||||
w={{ base: "100%", xs: 200 }}
|
||||
loading={loading}
|
||||
>
|
||||
Save
|
||||
</ConfirmButton>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -177,8 +177,8 @@ export default function Config<T>({
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={original}
|
||||
content={update}
|
||||
original={original}
|
||||
update={update}
|
||||
onConfirm={onConfirm}
|
||||
disabled={disabled}
|
||||
fileContentsLanguage={fileContentsLanguage}
|
||||
|
||||
@@ -70,7 +70,7 @@ const ConfirmButton = createPolymorphicComponent<"button", ConfirmButtonProps>(
|
||||
clickedOnce ? (
|
||||
<Check size="1rem" />
|
||||
) : loading ? (
|
||||
<Loader color="white" size="1rem" />
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
(rightSection ?? icon ?? <ICONS.Unknown size="1rem" />)
|
||||
)
|
||||
|
||||
@@ -116,16 +116,21 @@ export default function ConfirmModal({
|
||||
{additional}
|
||||
|
||||
<Group justify="end">
|
||||
<ConfirmButton
|
||||
icon={icon}
|
||||
disabled={disabled || input !== confirmText}
|
||||
onClick={() => {
|
||||
<Button
|
||||
justify="space-between"
|
||||
w={{ base: "100%", xs: 190 }}
|
||||
miw="fit-content"
|
||||
rightSection={
|
||||
loading ? <Loader color="white" size="1rem" /> : icon
|
||||
}
|
||||
disabled={loading || disabled || input !== confirmText}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfirm ? onConfirm().then(() => close()) : close();
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
{confirmButtonContent ?? children}
|
||||
</ConfirmButton>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -65,7 +65,6 @@ export default function CreateModal({
|
||||
</Modal>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={leftSection || <ICONS.Create size="1rem" />}
|
||||
onClick={open}
|
||||
w={{ base: "100%", xs: "fit-content" }}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Badge, Group, Switch, SwitchProps } from "@mantine/core";
|
||||
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({
|
||||
@@ -14,6 +15,7 @@ export default function EnableSwitch({
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
redDisabled = true,
|
||||
labelProps,
|
||||
...props
|
||||
}: EnableSwitchProps) {
|
||||
return (
|
||||
@@ -22,7 +24,7 @@ export default function EnableSwitch({
|
||||
checked={checked}
|
||||
color={color}
|
||||
label={
|
||||
<Group>
|
||||
<Group gap="sm" wrap="nowrap" {...labelProps}>
|
||||
{label}
|
||||
<Badge
|
||||
color={checked ? color : redDisabled ? "red" : "gray"}
|
||||
|
||||
@@ -78,12 +78,13 @@ export function MobileFriendlyTabsSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
changeAt: _changeAt,
|
||||
fullIconSize = "1.3rem",
|
||||
fullIconSize = "1.1rem",
|
||||
mobileIconSize = "1rem",
|
||||
tabProps,
|
||||
}: MobileFriendlyTabsSelectorProps) {
|
||||
const tabs = _tabs.filter((t) => !t.hidden);
|
||||
const changeAt = _changeAt ?? (tabs.length > 4 ? "lg" : "md");
|
||||
const changeAt =
|
||||
_changeAt ?? (tabs.length > 6 ? "lg" : tabs.length > 3 ? "md" : "sm");
|
||||
const SelectedIcon = tabs.find((tab) => tab.value === value)?.icon;
|
||||
return (
|
||||
<>
|
||||
@@ -100,10 +101,12 @@ export function MobileFriendlyTabsSelector({
|
||||
>
|
||||
<Group
|
||||
gap="xs"
|
||||
fz="h3"
|
||||
fz="lg"
|
||||
justify="center"
|
||||
c={tabValue === value ? undefined : "dimmed"}
|
||||
w={tabProps?.w ?? 140}
|
||||
w={100}
|
||||
miw="fit-content"
|
||||
wrap="nowrap"
|
||||
{...tabProps}
|
||||
>
|
||||
{Icon && <Icon size={fullIconSize} />}
|
||||
@@ -116,9 +119,9 @@ export function MobileFriendlyTabsSelector({
|
||||
</Group>
|
||||
|
||||
{/* MOBILE VIEW */}
|
||||
<Stack hiddenFrom={changeAt}>
|
||||
<Stack hiddenFrom={changeAt} w="100%">
|
||||
<Select
|
||||
width="100%"
|
||||
w={{ base: "100%", md: 300 }}
|
||||
value={value}
|
||||
onChange={(value) => value && onValueChange(value)}
|
||||
leftSection={SelectedIcon && <SelectedIcon size="1rem" />}
|
||||
@@ -137,6 +140,10 @@ export function MobileFriendlyTabsSelector({
|
||||
</Group>
|
||||
);
|
||||
}}
|
||||
withScrollArea={false}
|
||||
styles={{
|
||||
dropdown: { maxHeight: "calc(100vh - 230px)", overflowY: "auto" },
|
||||
}}
|
||||
/>
|
||||
{actions}
|
||||
</Stack>
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function ShowHideButton({
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShow(!show)}
|
||||
rightSection={
|
||||
show ? <ChevronUp className="w-4" /> : <ChevronDown className="w-4" />
|
||||
|
||||
Reference in New Issue
Block a user