Files
komodo/frontend/src/components/resources/stack/info.tsx
Maxwell Becker 98d72fc908 1.19.4 (#812)
* start 1.19.4

* deploy 1.19.4-dev-1

* try smaller binaries with cargo strip

* deploy 1.19.4-dev-2

* smaller binaries with cargo strip

* Fix Submit Dialog Button Behavior with 500 Errors on Duplicate Names (#819)

* Implement enhanced error handling and messaging for resource creation

* Implement improved error handling for resource creation across alerter, build, and sync

* Implement error handling improvements for resource copying and validation feedback

* Adjust error handling for resource creation to distinguish validation errors from unexpected system errors

* Refactor resource creation error handling by removing redundant match statements and simplifying the error propagation in multiple API modules.

* fmt

* bump indexmap

* fix account selector showing empty when account no longer found

* clean up theme logic, ensure monaco and others get up to date current theme

* enforce disable_non_admin_create for tags. Clean up status code responses

* update server cache concurrency controller

* deploy 1.19.4-dev-3

* Allow signing in by pressing enter (#830)

* Improve dialog overflow handling to prevent clipping of content (#828)

* Add Email notification entry to community.md (#824)

* Add clickable file path to show/hide file contents in StackInfo (#827)

* add clickable file path to show/hide file contents in StackInfo

Also added CopyButton due to the new functionality making the file path not selectable.

* Move clicking interaction to CardHeader

* Avoid sync edge cases of having toggle show function capturing showContents from outside

Co-authored-by: Maxwell Becker <49575486+mbecker20@users.noreply.github.com>

* Format previous change

* Add `default_show_contents` to `handleToggleShow`

---------

Co-authored-by: Maxwell Becker <49575486+mbecker20@users.noreply.github.com>

* deploy 1.19.4-dev-4

* avoid stake info ShowHideButton double toggle

* Allow multiple simultaneous Action runs for use with Args

* deploy 1.19.4-dev-5

* feat: persist all table sorting states including unsorted (#832)

- Always save sorting state to localStorage, even when empty/unsorted
- Fixes issue where 'unsorted' state was not persisted across page reloads
- Ensures consistent and predictable sorting behavior for all DataTable components

* autofocus on login username field (#837)

* Fix unnecessary auth queries flooding console on login page (#842)

* Refactor authentication error handling to use serror::Result and status codes

* Enable user query only when JWT is present

* Enable query execution in useRead only if JWT is present

* Revert backend auth changes - keep PR focused on frontend only

* Fix unnecessary API queries to unreachable servers flooding console (#843)

* Implement server availability checks in various components

* Refactor server availability check to ensure only healthy servers are identified

* cargo fmt

* fmt

* Auth error handling with status codes (#841)

* Refactor authentication error handling to use serror::Result and status codes

* Refactor error messages

* Refactor authentication error handling to include status codes and improve error messages

* clean up

* clean

* fmt

* invalid user id also UNAUTHORIZED

* deploy 1.19.4-dev-6

* deploy 1.19.4-dev-7

---------

Co-authored-by: Marcel Pfennig <82059270+MP-Tool@users.noreply.github.com>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
Co-authored-by: Guten <ywzhaifei@gmail.com>
Co-authored-by: Paulo Roberto Albuquerque <paulora2405@gmail.com>
Co-authored-by: Lorenzo Farnararo <2814802+baldarn@users.noreply.github.com>
2025-09-14 12:32:06 -07:00

327 lines
12 KiB
TypeScript

import { Section } from "@components/layouts";
import { ReactNode, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { useFullStack, useStack } from ".";
import { cn, updateLogToHtml } from "@lib/utils";
import { language_from_path, MonacoEditor } from "@components/monaco";
import { usePermissions } from "@lib/hooks";
import { ConfirmUpdate } from "@components/config/util";
import { useLocalStorage, useWrite } from "@lib/hooks";
import { Button } from "@ui/button";
import { FilePlus, History } from "lucide-react";
import { useToast } from "@ui/use-toast";
import { ConfirmButton, ShowHideButton, CopyButton } from "@components/util";
import { DEFAULT_STACK_FILE_CONTENTS } from "./config";
import { Types } from "komodo_client";
export const StackInfo = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const [edits, setEdits] = useLocalStorage<Record<string, string | undefined>>(
`stack-${id}-edits`,
{}
);
const [show, setShow] = useState<Record<string, boolean | undefined>>({});
const { canWrite } = usePermissions({ type: "Stack", id });
const { toast } = useToast();
const { mutateAsync, isPending } = useWrite("WriteStackFileContents", {
onSuccess: (res) => {
toast({
title: res.success ? "Contents written." : "Failed to write contents.",
variant: res.success ? undefined : "destructive",
});
},
});
const not_down = useStack(id)?.info.state !== Types.StackState.Down;
const stack = useFullStack(id);
// const state = useStack(id)?.info.state ?? Types.StackState.Unknown;
// const is_down = [Types.StackState.Down, Types.StackState.Unknown].includes(
// state
// );
const file_on_host = stack?.config?.files_on_host ?? false;
const git_repo = !!(stack?.config?.repo || stack?.config?.linked_repo);
const canEdit = canWrite && (file_on_host || git_repo);
const editFileCallback = (path: string) => (contents: string) =>
setEdits({ ...edits, [path]: contents });
// Collect deployed / latest contents, joining
// them by path.
// Only unmatched latest contents end up in latest_contents.
// const deployed_contents: {
// path: string;
// deployed: string;
// modified: string | undefined;
// }[] = [];
// if (!is_down) {
// for (const content of stack?.info?.deployed_contents ?? []) {
// const latest = stack?.info?.remote_contents?.find(
// (latest) => latest.path === content.path
// );
// const modified =
// latest?.contents &&
// (latest.contents !== content.contents ? latest.contents : undefined);
// deployed_contents.push({
// path: content.path,
// deployed: content.contents,
// modified,
// });
// }
// }
const latest_contents = stack?.info?.remote_contents;
const latest_errors = stack?.info?.remote_errors;
// Contents will be default hidden if there is more than 2 file editor to show
const default_show_contents = !latest_contents || latest_contents.length < 3;
return (
<Section titleOther={titleOther}>
{/* Errors */}
{latest_errors &&
latest_errors.length > 0 &&
latest_errors.map((error) => (
<Card key={error.path} className="flex flex-col gap-4">
<CardHeader className="flex flex-row justify-between items-center pb-0">
<div className="font-mono flex gap-2">
<div className="text-muted-foreground">Path:</div>
{error.path}
</div>
{canEdit && (
<ConfirmButton
title="Initialize File"
icon={<FilePlus className="w-4 h-4" />}
onClick={() => {
if (stack) {
mutateAsync({
stack: stack.name,
file_path: error.path,
contents: DEFAULT_STACK_FILE_CONTENTS,
});
}
}}
loading={isPending}
/>
)}
</CardHeader>
<CardContent className="pr-8">
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(error.contents),
}}
className="max-h-[500px] overflow-y-auto"
/>
</CardContent>
</Card>
))}
{/* Update deployed contents with diff */}
{/* {!is_down && deployed_contents.length > 0 && (
<Card>
<CardHeader className="flex flex-col gap-2">
deployed contents:{" "}
</CardHeader>
<CardContent>
{deployed_contents.map((content) => {
return (
<pre key={content.path} className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<div>path: {content.path}</div>
{canEdit && (
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() =>
setEdits({ ...edits, [content.path]: undefined })
}
className="flex items-center gap-2"
disabled={!edits[content.path]}
>
<History className="w-4 h-4" />
Reset
</Button>
<ConfirmUpdate
previous={{
contents: content.modified ?? content.deployed,
}}
content={{ contents: edits[content.path] }}
onConfirm={() => {
if (stack) {
mutateAsync({
stack: stack.name,
file_path: content.path,
contents: edits[content.path]!,
}).then(() =>
setEdits({
...edits,
[content.path]: undefined,
})
);
}
}}
disabled={!edits[content.path]}
/>
</div>
)}
</div>
{content.modified ? (
<MonacoDiffEditor
original={"# Deployed contents\n" + content.deployed}
modified={edits[content.path] ?? content.modified}
language="yaml"
readOnly={!canEdit}
hideUnchangedRegions={false}
onModifiedValueChange={editFileCallback(content.path)}
/>
) : (
<MonacoEditor
value={edits[content.path] ?? content.deployed}
language="yaml"
readOnly={!canEdit}
onValueChange={editFileCallback(content.path)}
/>
)}
</pre>
);
})}
</CardContent>
</Card>
)} */}
{/* Update latest contents */}
{latest_contents &&
latest_contents.length > 0 &&
latest_contents.map((content) => {
const showContents = show[content.path] ?? default_show_contents;
const handleToggleShow = () => {
setShow((show) => ({
...show,
[content.path]: !(show[content.path] ?? default_show_contents),
}));
};
return (
<Card key={content.path} className="flex flex-col gap-4">
<CardHeader
className={cn(
"flex flex-row justify-between items-center group cursor-pointer",
showContents && "pb-0"
)}
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();
}
}}
>
<CardTitle className="font-mono flex gap-2 items-center">
<div className="flex gap-2 items-center">
<span className="text-muted-foreground">File:</span>
<span>{content.path}</span>
<span onClick={(e) => e.stopPropagation()} data-copy-button>
<CopyButton content={content.path} label="file path" />
</span>
</div>
</CardTitle>
<div className="flex items-center gap-2">
{canEdit && (
<>
<Button
variant="outline"
onClick={(e) => {
e.stopPropagation();
setEdits({ ...edits, [content.path]: undefined });
}}
className="flex items-center gap-2"
disabled={!edits[content.path]}
>
<History className="w-4 h-4" />
Reset
</Button>
<span onClick={(e) => e.stopPropagation()}>
<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}
/>
</span>
</>
)}
<ShowHideButton
show={showContents}
setShow={() => {}}
/>
</div>
</CardHeader>
{showContents && (
<CardContent className="pr-8">
<MonacoEditor
value={edits[content.path] ?? content.contents}
language={language_from_path(content.path)}
readOnly={!canEdit}
filename={content.path}
onValueChange={editFileCallback(content.path)}
/>
</CardContent>
)}
</Card>
);
})}
{stack?.info?.deployed_config && not_down && (
<Card className="flex flex-col gap-4">
<CardHeader className="pb-0">
<CardTitle className="font-mono">Deployed config:</CardTitle>
<CardDescription>
Output of '<code>docker compose config</code>' when Stack was last
deployed.
</CardDescription>
</CardHeader>
<CardContent className="pr-8">
<MonacoEditor
value={stack.info.deployed_config}
language="yaml"
readOnly
/>
</CardContent>
</Card>
)}
</Section>
);
};