mirror of
https://github.com/moghtech/komodo.git
synced 2026-09-09 08:01:31 +00:00
223 lines
7.6 KiB
TypeScript
223 lines
7.6 KiB
TypeScript
import { usePermissions, useWrite } from "@/lib/hooks";
|
|
import { useLocalStorage } from "@mantine/hooks";
|
|
import { notifications } from "@mantine/notifications";
|
|
import { ReactNode, useState } from "react";
|
|
import { DEFAULT_STACK_FILE_CONTENTS, useFullStack, useStack } from ".";
|
|
import { Types } from "komodo_client";
|
|
import Section from "@/ui/section";
|
|
import { Button, Code, Group, Stack, Text } from "@mantine/core";
|
|
import ConfirmButton from "@/ui/confirm-button";
|
|
import { FilePlus } from "lucide-react";
|
|
import { updateLogToHtml } from "@/lib/utils";
|
|
import CopyButton from "@/ui/copy-button";
|
|
import { ICONS } from "@/theme/icons";
|
|
import ConfirmUpdate from "@/ui/config/confirm";
|
|
import ShowHideButton from "@/ui/show-hide-button";
|
|
import { languageFromPath, MonacoEditor } from "@/components/monaco";
|
|
|
|
export default function StackInfo({
|
|
id,
|
|
titleOther,
|
|
}: {
|
|
id: string;
|
|
titleOther: ReactNode;
|
|
}) {
|
|
const [edits, setEdits] = useLocalStorage<Record<string, string | undefined>>(
|
|
{ key: `stack-${id}-edits`, defaultValue: {} },
|
|
);
|
|
const [show, setShow] = useState<Record<string, boolean | undefined>>({});
|
|
const { canWrite } = usePermissions({ type: "Stack", id });
|
|
|
|
const { mutateAsync, isPending } = useWrite("WriteStackFileContents", {
|
|
onSuccess: (res) => {
|
|
notifications.show({
|
|
message: res.success
|
|
? "Contents written."
|
|
: "Failed to write contents.",
|
|
color: res.success ? "green" : "red",
|
|
});
|
|
},
|
|
});
|
|
|
|
const notDown = useStack(id)?.info.state !== Types.StackState.Down;
|
|
const stack = useFullStack(id);
|
|
|
|
const filesOnHost = stack?.config?.files_on_host ?? false;
|
|
const gitRepo = !!(stack?.config?.repo || stack?.config?.linked_repo);
|
|
const canEdit = canWrite && (filesOnHost || gitRepo);
|
|
const editFileCallback = (path: string) => (contents: string) =>
|
|
setEdits({ ...edits, [path]: contents });
|
|
|
|
const latestContents = stack?.info?.remote_contents;
|
|
const latestErrors = stack?.info?.remote_errors;
|
|
|
|
// Contents will be default hidden if there is more than 2 file editor to show
|
|
const defaultShowContents = !latestContents || latestContents.length < 3;
|
|
|
|
return (
|
|
<Section titleOther={titleOther}>
|
|
{/* Errors */}
|
|
{latestErrors &&
|
|
latestErrors.length > 0 &&
|
|
latestErrors.map((error) => (
|
|
<Stack key={error.path} className="bordered-light" bdrs="md" p="xl">
|
|
{/* HEADER */}
|
|
<Group justify="between">
|
|
<Group ff="monospace">
|
|
<Text c="dimmed">Path:</Text>
|
|
{error.path}
|
|
</Group>
|
|
|
|
{canEdit && (
|
|
<ConfirmButton
|
|
loading={isPending}
|
|
icon={<FilePlus size="1rem" />}
|
|
onClick={() => {
|
|
if (stack) {
|
|
mutateAsync({
|
|
stack: stack.name,
|
|
file_path: error.path,
|
|
contents: DEFAULT_STACK_FILE_CONTENTS,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Initialize File
|
|
</ConfirmButton>
|
|
)}
|
|
</Group>
|
|
|
|
{/* CONTENTS */}
|
|
<Code
|
|
component="pre"
|
|
fz="sm"
|
|
mah={500}
|
|
dangerouslySetInnerHTML={{
|
|
__html: updateLogToHtml(error.contents),
|
|
}}
|
|
style={{ overflowY: "auto" }}
|
|
/>
|
|
</Stack>
|
|
))}
|
|
|
|
{/* Update latest contents */}
|
|
{latestContents &&
|
|
latestContents.map((content) => {
|
|
const showContents = show[content.path] ?? defaultShowContents;
|
|
const handleToggleShow = () => {
|
|
setShow((show) => ({
|
|
...show,
|
|
[content.path]: !(show[content.path] ?? defaultShowContents),
|
|
}));
|
|
};
|
|
return (
|
|
<Stack
|
|
key={content.path}
|
|
className="bordered-light"
|
|
bdrs="md"
|
|
p="xl"
|
|
>
|
|
{/* HEADER */}
|
|
<Group
|
|
justify="space-between"
|
|
style={{ cursor: "pointer" }}
|
|
onClick={handleToggleShow}
|
|
tabIndex={0}
|
|
role="button"
|
|
aria-pressed={showContents}
|
|
onKeyDown={(e) => {
|
|
if (
|
|
(e.key === "Enter" || e.key === " ") &&
|
|
e.target === e.currentTarget
|
|
) {
|
|
if (e.key === " ") e.preventDefault();
|
|
handleToggleShow();
|
|
}
|
|
}}
|
|
>
|
|
{/* PATH */}
|
|
<Group ff="monospace">
|
|
<Text c="dimmed">File:</Text>
|
|
{content.path}
|
|
<CopyButton label="file path" content={content.path} />
|
|
</Group>
|
|
|
|
{/* SAVE */}
|
|
<Group>
|
|
{canEdit && (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
leftSection={<ICONS.History size="1rem" />}
|
|
disabled={!edits[content.path]}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setEdits({ ...edits, [content.path]: undefined });
|
|
}}
|
|
>
|
|
Reset
|
|
</Button>
|
|
<ConfirmUpdate
|
|
previous={{ contents: content.contents }}
|
|
content={{ contents: edits[content.path] }}
|
|
onConfirm={async () => {
|
|
if (stack) {
|
|
return await mutateAsync({
|
|
stack: stack.name,
|
|
file_path: content.path,
|
|
contents: edits[content.path]!,
|
|
}).then(() =>
|
|
setEdits({
|
|
...edits,
|
|
[content.path]: undefined,
|
|
}),
|
|
);
|
|
}
|
|
}}
|
|
disabled={!edits[content.path]}
|
|
language="yaml"
|
|
loading={isPending}
|
|
/>
|
|
</>
|
|
)}
|
|
{/* The toggle onClick is given to entire header */}
|
|
<ShowHideButton show={showContents} setShow={() => {}} />
|
|
</Group>
|
|
</Group>
|
|
|
|
{/* CONTENTS */}
|
|
{showContents && (
|
|
<MonacoEditor
|
|
value={edits[content.path] ?? content.contents}
|
|
language={languageFromPath(content.path)}
|
|
readOnly={!canEdit}
|
|
filename={content.path}
|
|
onValueChange={editFileCallback(content.path)}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
);
|
|
})}
|
|
|
|
{stack?.info?.deployed_config && notDown && (
|
|
<Stack className="bordered-light" bdrs="md" p="xl">
|
|
{/* HEADER */}
|
|
<Stack ff="monospace" gap="0">
|
|
<Text fz="md">Deployed config:</Text>
|
|
<Text fz="sm" c="dimmed">
|
|
Output of '<code>docker compose config</code>' when Stack was last
|
|
deployed.
|
|
</Text>
|
|
</Stack>
|
|
|
|
<MonacoEditor
|
|
value={stack.info.deployed_config}
|
|
language="yaml"
|
|
readOnly
|
|
/>
|
|
</Stack>
|
|
)}
|
|
</Section>
|
|
);
|
|
}
|