From f1f5f881b45e4b31212426325c428b278f26cf01 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Mon, 1 Jul 2024 22:31:19 +0000
Subject: [PATCH 01/19] fix: get basic fix in for preventing download logs
from blowing up UI
---
site/src/modules/resources/AgentLogs/useAgentLogs.ts | 7 +++++--
.../WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx | 6 +++++-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
index e5d797a14e9c2..40f9f29e8bc18 100644
--- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts
+++ b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
@@ -29,8 +29,11 @@ export function useAgentLogs(
const lastQueriedLogId = useRef(0);
useEffect(() => {
- if (logs && lastQueriedLogId.current === 0) {
- lastQueriedLogId.current = logs[logs.length - 1].id;
+ const lastLog = logs?.at(-1);
+ const canSetLogId = lastLog !== undefined && lastQueriedLogId.current === 0;
+
+ if (canSetLogId) {
+ lastQueriedLogId.current = lastLog.id;
}
}, [logs]);
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index aefd4d7d6b9e1..4e59f75516a4b 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -33,17 +33,20 @@ export const DownloadLogsDialog: FC = ({
}) => {
const theme = useTheme();
const agents = selectAgents(workspace);
+
const agentLogResults = useQueries({
queries: agents.map((a) => ({
...agentLogs(workspace.id, a.id),
enabled: dialogProps.open,
})),
});
+
const buildLogsQuery = useQuery({
...buildLogs(workspace),
enabled: dialogProps.open,
});
- const downloadableFiles: DownloadableFile[] = useMemo(() => {
+
+ const downloadableFiles = useMemo(() => {
const files: DownloadableFile[] = [
{
name: `${workspace.name}-build-logs.txt`,
@@ -68,6 +71,7 @@ export const DownloadLogsDialog: FC = ({
return files;
}, [agentLogResults, agents, buildLogsQuery.data, workspace.name]);
+
const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
const [isDownloading, setIsDownloading] = useState(false);
From edd636232849f847576f7d77feeaee8d716e7e7e Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Mon, 1 Jul 2024 23:15:37 +0000
Subject: [PATCH 02/19] fix: make sure blob units can't go out of bounds
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 4e59f75516a4b..b090fc1b6b384 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -13,6 +13,8 @@ import {
import { displayError } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
+const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
+
type DownloadLogsDialogProps = Pick<
ConfirmDialogProps,
"onConfirm" | "onClose" | "open"
@@ -32,7 +34,9 @@ export const DownloadLogsDialog: FC = ({
...dialogProps
}) => {
const theme = useTheme();
- const agents = selectAgents(workspace);
+ const agents = workspace.latest_build.resources.flatMap(
+ (resource) => resource.agents ?? [],
+ );
const agentLogResults = useQueries({
queries: agents.map((a) => ({
@@ -131,19 +135,14 @@ export const DownloadLogsDialog: FC = ({
};
function humanBlobSize(size: number) {
- const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
- while (size > 1024 && i < units.length) {
+ while (size > 1024 && i < BLOB_SIZE_UNITS.length) {
size /= 1024;
i++;
}
- return `${size.toFixed(2)} ${units[i]}`;
-}
-function selectAgents(workspace: Workspace): WorkspaceAgent[] {
- return workspace.latest_build.resources
- .flatMap((r) => r.agents)
- .filter((a) => a !== undefined) as WorkspaceAgent[];
+ const finalUnit = BLOB_SIZE_UNITS[i] ?? BLOB_SIZE_UNITS.at(-1) ?? "TB";
+ return `${size.toFixed(2)} ${finalUnit}`;
}
const styles = {
From 901aa39c9a220d47618e20a713afd5dd90ca0497 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Mon, 1 Jul 2024 23:18:41 +0000
Subject: [PATCH 03/19] fix: make sure timeout is cleared on component unmount
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index b090fc1b6b384..c9484c0b47f8b 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -2,7 +2,7 @@ import { useTheme, type Interpolation, type Theme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
-import { useMemo, useState, type FC } from "react";
+import { useMemo, useState, type FC, useRef, useEffect } from "react";
import { useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
@@ -79,6 +79,15 @@ export const DownloadLogsDialog: FC = ({
const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
const [isDownloading, setIsDownloading] = useState(false);
+ const timeoutIdRef = useRef(undefined);
+ useEffect(() => {
+ const clearTimeoutOnUnmount = () => {
+ window.clearTimeout(timeoutIdRef.current);
+ };
+
+ return clearTimeoutOnUnmount;
+ }, []);
+
return (
= ({
zip.file(f.name, f.blob);
}
});
+
const content = await zip.generateAsync({ type: "blob" });
download(content, `${workspace.name}-logs.zip`);
dialogProps.onClose();
- setTimeout(() => {
+
+ timeoutIdRef.current = window.setTimeout(() => {
setIsDownloading(false);
}, theme.transitions.duration.leavingScreen);
} catch (error) {
From e1d811361a111780507062193a5d84c58ca87706 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 01:50:33 +0000
Subject: [PATCH 04/19] fix: reduce risk of shared cache state breaking
useAgentLogs
---
.../resources/AgentLogs/useAgentLogs.ts | 22 ++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
index 40f9f29e8bc18..98673abbf114a 100644
--- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts
+++ b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
@@ -15,17 +15,29 @@ export type UseAgentLogsOptions = Readonly<{
enabled?: boolean;
}>;
+/**
+ * Defines a custom hook that gives you all workspace agent logs for a given
+ * workspace.
+ *
+ * Depending on the status of the workspace, all logs may or may not be
+ * available.
+ */
export function useAgentLogs(
options: UseAgentLogsOptions,
): readonly WorkspaceAgentLog[] | undefined {
const { workspaceId, agentId, agentLifeCycleState, enabled = true } = options;
+
const queryClient = useQueryClient();
const queryOptions = agentLogs(workspaceId, agentId);
- const query = useQuery({
- ...queryOptions,
- enabled,
- });
- const logs = query.data;
+ const query = useQuery({ ...queryOptions, enabled });
+
+ // One pitfall with the current approach: the enabled property does NOT
+ // prevent the useQuery call above from eventually having data. All it does
+ // is prevent it from getting data on its own. If a different useQuery call
+ // elsewhere in the app is enabled and gets data, the useQuery call here will
+ // re-render with that same new data, even if it's disabled. This can EASILY
+ // cause bugs.
+ const logs = enabled ? query.data : undefined;
const lastQueriedLogId = useRef(0);
useEffect(() => {
From 87f57aa970b1ae3e993982f4274f7943407fca8b Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 03:22:27 +0000
Subject: [PATCH 05/19] fix: allow partial downloading of logs
---
site/src/api/errors.ts | 6 +--
.../WorkspaceActions/DownloadLogsDialog.tsx | 46 ++++++++++++++-----
2 files changed, 37 insertions(+), 15 deletions(-)
diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts
index ada591d754fb2..65f450c644902 100644
--- a/site/src/api/errors.ts
+++ b/site/src/api/errors.ts
@@ -110,15 +110,15 @@ export const getValidationErrorMessage = (error: unknown): string => {
return validationErrors.map((error) => error.detail).join("\n");
};
-export const getErrorDetail = (error: unknown): string | undefined | null => {
+export const getErrorDetail = (error: unknown): string | null => {
if (error instanceof Error) {
return "Please check the developer console for more details.";
}
if (isApiError(error)) {
- return error.response.data.detail;
+ return error.response.data.detail ?? null;
}
if (isApiErrorResponse(error)) {
- return error.detail;
+ return error.detail ?? null;
}
return null;
};
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index c9484c0b47f8b..29a37d252800d 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -12,6 +12,7 @@ import {
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
+import { ErrorAlert } from "components/Alert/ErrorAlert";
const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
@@ -76,9 +77,7 @@ export const DownloadLogsDialog: FC = ({
return files;
}, [agentLogResults, agents, buildLogsQuery.data, workspace.name]);
- const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
const [isDownloading, setIsDownloading] = useState(false);
-
const timeoutIdRef = useRef(undefined);
useEffect(() => {
const clearTimeoutOnUnmount = () => {
@@ -88,24 +87,37 @@ export const DownloadLogsDialog: FC = ({
return clearTimeoutOnUnmount;
}, []);
+ const isWorkspaceHealthy = workspace.health.healthy;
+ const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
+
return (
+ Download
+ {!isWorkspaceHealthy && <> {isLoadingFiles ? "partial" : "all"}>}
+ >
+ }
+ disabled={
+ isDownloading ||
+ // If a workspace isn't healthy, let the user download as many logs as
+ // they can
+ (isWorkspaceHealthy && isLoadingFiles)
+ }
onConfirm={async () => {
- try {
- setIsDownloading(true);
- const zip = new JSZip();
- downloadableFiles.forEach((f) => {
- if (f.blob) {
- zip.file(f.name, f.blob);
- }
- });
+ setIsDownloading(true);
+ const zip = new JSZip();
+ downloadableFiles.forEach((f) => {
+ if (f.blob) {
+ zip.file(f.name, f.blob);
+ }
+ });
+ try {
const content = await zip.generateAsync({ type: "blob" });
download(content, `${workspace.name}-logs.zip`);
dialogProps.onClose();
@@ -125,6 +137,16 @@ export const DownloadLogsDialog: FC = ({
Downloading logs will create a zip file containing all logs from all
jobs in this workspace. This may take a while.
+
+ {!isWorkspaceHealthy && isLoadingFiles && (
+ <>
+
+ >
+ )}
+
{downloadableFiles.map((f) => (
-
From 5bf2bafe7cbf89ddfe95201fd50bb5c1cf6f2fe5 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 03:50:26 +0000
Subject: [PATCH 06/19] fix: make sure useMemo cache is used properly
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 96 ++++++++++++-------
1 file changed, 63 insertions(+), 33 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 29a37d252800d..44de59d0fd8ed 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -3,9 +3,13 @@ import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { useMemo, useState, type FC, useRef, useEffect } from "react";
-import { useQueries, useQuery } from "react-query";
+import { UseQueryOptions, useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
-import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
+import type {
+ Workspace,
+ WorkspaceAgent,
+ WorkspaceAgentLog,
+} from "api/typesGenerated";
import {
ConfirmDialog,
type ConfirmDialogProps,
@@ -32,50 +36,75 @@ type DownloadableFile = {
export const DownloadLogsDialog: FC = ({
workspace,
download = saveAs,
- ...dialogProps
+ open,
+ onConfirm,
+ onClose,
}) => {
const theme = useTheme();
- const agents = workspace.latest_build.resources.flatMap(
- (resource) => resource.agents ?? [],
- );
-
- const agentLogResults = useQueries({
- queries: agents.map((a) => ({
- ...agentLogs(workspace.id, a.id),
- enabled: dialogProps.open,
- })),
- });
const buildLogsQuery = useQuery({
...buildLogs(workspace),
- enabled: dialogProps.open,
+ enabled: open,
});
-
- const downloadableFiles = useMemo(() => {
- const files: DownloadableFile[] = [
- {
- name: `${workspace.name}-build-logs.txt`,
- blob: buildLogsQuery.data
- ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
- type: "text/plain",
- })
- : undefined,
- },
+ const buildLogsFile = useMemo(() => {
+ return {
+ name: `${workspace.name}-build-logs.txt`,
+ blob: buildLogsQuery.data
+ ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
+ type: "text/plain",
+ })
+ : undefined,
+ };
+ }, [workspace.name, buildLogsQuery.data]);
+
+ // This is clunky, but we have to memoize in two steps to make sure that we
+ // don't accidentally break the memo cache every render. We can't tuck
+ // everything into a single memo call, because we need to set up React Query
+ // state between processing the agents, and we can't violate rules of hooks
+ type AgentInfo = Readonly<{
+ agents: readonly WorkspaceAgent[];
+ queries: readonly UseQueryOptions[];
+ }>;
+
+ const { agents, queries } = useMemo(() => {
+ const allAgents = workspace.latest_build.resources.flatMap(
+ (resource) => resource.agents ?? [],
+ );
+
+ // Can't use the "new Set()" trick because we're not dealing with primitives
+ const uniqueAgents = [
+ ...new Map(allAgents.map((agent) => [agent.id, agent])).values(),
];
+ return {
+ agents: uniqueAgents,
+ queries: uniqueAgents.map((agent) => {
+ return {
+ ...agentLogs(workspace.id, agent.id),
+ enabled: open,
+ };
+ }),
+ };
+ }, [workspace, open]);
+
+ const agentLogResults = useQueries({ queries });
+ const allFiles = useMemo(() => {
+ const files: DownloadableFile[] = [buildLogsFile];
+
agents.forEach((a, i) => {
const name = `${a.name}-logs.txt`;
- const logs = agentLogResults[i].data;
- const txt = logs?.map((l) => l.output).join("\n");
+ const txt = agentLogResults[i]?.data?.map((l) => l.output).join("\n");
+
let blob: Blob | undefined;
if (txt) {
blob = new Blob([txt], { type: "text/plain" });
}
+
files.push({ name, blob });
});
return files;
- }, [agentLogResults, agents, buildLogsQuery.data, workspace.name]);
+ }, [agentLogResults, agents, buildLogsFile]);
const [isDownloading, setIsDownloading] = useState(false);
const timeoutIdRef = useRef(undefined);
@@ -88,11 +117,12 @@ export const DownloadLogsDialog: FC = ({
}, []);
const isWorkspaceHealthy = workspace.health.healthy;
- const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
+ const isLoadingFiles = allFiles.some((f) => f.blob === undefined);
return (
= ({
onConfirm={async () => {
setIsDownloading(true);
const zip = new JSZip();
- downloadableFiles.forEach((f) => {
+ allFiles.forEach((f) => {
if (f.blob) {
zip.file(f.name, f.blob);
}
@@ -120,7 +150,7 @@ export const DownloadLogsDialog: FC = ({
try {
const content = await zip.generateAsync({ type: "blob" });
download(content, `${workspace.name}-logs.zip`);
- dialogProps.onClose();
+ onClose();
timeoutIdRef.current = window.setTimeout(() => {
setIsDownloading(false);
@@ -148,7 +178,7 @@ export const DownloadLogsDialog: FC = ({
)}
- {downloadableFiles.map((f) => (
+ {allFiles.map((f) => (
-
{f.name}
From 5c2316b75ca825cbbeda2a228184758e94b40b8e Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 04:20:03 +0000
Subject: [PATCH 07/19] wip: commit current progress on updated logs
functionality
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 85 +++++++++++++------
1 file changed, 61 insertions(+), 24 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 44de59d0fd8ed..a5c8a5598fd89 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -35,10 +35,9 @@ type DownloadableFile = {
export const DownloadLogsDialog: FC = ({
workspace,
- download = saveAs,
open,
- onConfirm,
onClose,
+ download = saveAs,
}) => {
const theme = useTheme();
@@ -107,18 +106,18 @@ export const DownloadLogsDialog: FC = ({
}, [agentLogResults, agents, buildLogsFile]);
const [isDownloading, setIsDownloading] = useState(false);
- const timeoutIdRef = useRef(undefined);
+ const isWorkspaceHealthy = workspace.health.healthy;
+ const isLoadingFiles = allFiles.some((f) => f.blob === undefined);
+
+ const resetDownloadStateIdRef = useRef(undefined);
useEffect(() => {
const clearTimeoutOnUnmount = () => {
- window.clearTimeout(timeoutIdRef.current);
+ window.clearTimeout(resetDownloadStateIdRef.current);
};
return clearTimeoutOnUnmount;
}, []);
- const isWorkspaceHealthy = workspace.health.healthy;
- const isLoadingFiles = allFiles.some((f) => f.blob === undefined);
-
return (
= ({
download(content, `${workspace.name}-logs.zip`);
onClose();
- timeoutIdRef.current = window.setTimeout(() => {
+ resetDownloadStateIdRef.current = window.setTimeout(() => {
setIsDownloading(false);
}, theme.transitions.duration.leavingScreen);
} catch (error) {
@@ -169,26 +168,16 @@ export const DownloadLogsDialog: FC = ({
{!isWorkspaceHealthy && isLoadingFiles && (
- <>
-
- >
+
)}
{allFiles.map((f) => (
- -
- {f.name}
-
- {f.blob ? (
- humanBlobSize(f.blob.size)
- ) : (
-
- )}
-
-
+
))}
@@ -197,6 +186,48 @@ export const DownloadLogsDialog: FC = ({
);
};
+type DownloadingItemProps = Readonly<{
+ // A value of undefined indicates that the component will wait forever
+ giveUpTimeMs?: number;
+ file: DownloadableFile;
+}>;
+
+const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
+ const [isWaiting, setIsWaiting] = useState(true);
+ useEffect(() => {
+ if (giveUpTimeMs === undefined || file.blob !== undefined) {
+ setIsWaiting(true);
+ return;
+ }
+
+ const timeoutId = window.setTimeout(
+ () => setIsWaiting(false),
+ giveUpTimeMs,
+ );
+
+ return () => window.clearTimeout(timeoutId);
+ }, [giveUpTimeMs, file]);
+
+ return (
+ -
+ {file.name}
+
+ {file.blob ? (
+ humanBlobSize(file.blob.size)
+ ) : (
+ <>
+ {isWaiting ? (
+
+ ) : (
+
N/A
+ )}
+ >
+ )}
+
+
+ );
+};
+
function humanBlobSize(size: number) {
let i = 0;
while (size > 1024 && i < BLOB_SIZE_UNITS.length) {
@@ -204,6 +235,8 @@ function humanBlobSize(size: number) {
i++;
}
+ // The while condition can break if we accidentally exceed the bounds of the
+ // array. Have to be extra sure we have a unit at the very end.
const finalUnit = BLOB_SIZE_UNITS[i] ?? BLOB_SIZE_UNITS.at(-1) ?? "TB";
return `${size.toFixed(2)} ${finalUnit}`;
}
@@ -229,4 +262,8 @@ const styles = {
listItemSecondary: {
fontSize: 14,
},
+
+ notAvailableText: (theme) => ({
+ color: theme.palette.error.main,
+ }),
} satisfies Record>;
From edd6569b8d4f571343dc7424d5b2ec34808fb237 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 04:23:23 +0000
Subject: [PATCH 08/19] docs: rewrite comment for clarity
---
site/src/modules/resources/AgentLogs/useAgentLogs.ts | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
index 98673abbf114a..8834300713000 100644
--- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts
+++ b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
@@ -33,9 +33,10 @@ export function useAgentLogs(
// One pitfall with the current approach: the enabled property does NOT
// prevent the useQuery call above from eventually having data. All it does
- // is prevent it from getting data on its own. If a different useQuery call
- // elsewhere in the app is enabled and gets data, the useQuery call here will
- // re-render with that same new data, even if it's disabled. This can EASILY
+ // is prevent it from getting data on its own. Let's say a different useQuery
+ // call elsewhere in the app has the same query key and is enabled. When it
+ // gets data back from the server, the useQuery call here will re-render with
+ // that same new data, even though this state is "disabled". This can EASILY
// cause bugs.
const logs = enabled ? query.data : undefined;
From 2eb7d6ebd818b5db561e5f9bae76c7c82268dbcd Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 13:22:57 +0000
Subject: [PATCH 09/19] refactor: clean up current code
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 29 +++++++++----------
1 file changed, 13 insertions(+), 16 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index a5c8a5598fd89..82e87d53379d7 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -2,7 +2,7 @@ import { useTheme, type Interpolation, type Theme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
-import { useMemo, useState, type FC, useRef, useEffect } from "react";
+import { type FC, useMemo, useState, useRef, useEffect } from "react";
import { UseQueryOptions, useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
import type {
@@ -59,13 +59,14 @@ export const DownloadLogsDialog: FC = ({
// This is clunky, but we have to memoize in two steps to make sure that we
// don't accidentally break the memo cache every render. We can't tuck
// everything into a single memo call, because we need to set up React Query
- // state between processing the agents, and we can't violate rules of hooks
+ // state between processing the agents, but we can't violate rules of hooks by
+ // putting hooks inside of hooks
type AgentInfo = Readonly<{
agents: readonly WorkspaceAgent[];
- queries: readonly UseQueryOptions[];
+ logOptionsArray: readonly UseQueryOptions[];
}>;
- const { agents, queries } = useMemo(() => {
+ const { agents, logOptionsArray } = useMemo(() => {
const allAgents = workspace.latest_build.resources.flatMap(
(resource) => resource.agents ?? [],
);
@@ -77,7 +78,7 @@ export const DownloadLogsDialog: FC = ({
return {
agents: uniqueAgents,
- queries: uniqueAgents.map((agent) => {
+ logOptionsArray: uniqueAgents.map((agent) => {
return {
...agentLogs(workspace.id, agent.id),
enabled: open,
@@ -86,13 +87,13 @@ export const DownloadLogsDialog: FC = ({
};
}, [workspace, open]);
- const agentLogResults = useQueries({ queries });
+ const agentLogQueries = useQueries({ queries: logOptionsArray });
const allFiles = useMemo(() => {
const files: DownloadableFile[] = [buildLogsFile];
agents.forEach((a, i) => {
const name = `${a.name}-logs.txt`;
- const txt = agentLogResults[i]?.data?.map((l) => l.output).join("\n");
+ const txt = agentLogQueries[i]?.data?.map((l) => l.output).join("\n");
let blob: Blob | undefined;
if (txt) {
@@ -103,7 +104,7 @@ export const DownloadLogsDialog: FC = ({
});
return files;
- }, [agentLogResults, agents, buildLogsFile]);
+ }, [agentLogQueries, agents, buildLogsFile]);
const [isDownloading, setIsDownloading] = useState(false);
const isWorkspaceHealthy = workspace.health.healthy;
@@ -134,7 +135,7 @@ export const DownloadLogsDialog: FC = ({
disabled={
isDownloading ||
// If a workspace isn't healthy, let the user download as many logs as
- // they can
+ // they can. Otherwise, wait for everything to come in
(isWorkspaceHealthy && isLoadingFiles)
}
onConfirm={async () => {
@@ -214,14 +215,10 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
{file.blob ? (
humanBlobSize(file.blob.size)
+ ) : isWaiting ? (
+
) : (
- <>
- {isWaiting ? (
-
- ) : (
- N/A
- )}
- >
+ N/A
)}
From fa06515a869c20f05f4790eac37008c110bbc140 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 13:42:08 +0000
Subject: [PATCH 10/19] fix: update styles for unavailable logs
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 36 +++++++++++++++++--
1 file changed, 33 insertions(+), 3 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 82e87d53379d7..34e070c324bf1 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -1,5 +1,6 @@
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
+import ErrorIcon from "@mui/icons-material/ErrorOutline";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { type FC, useMemo, useState, useRef, useEffect } from "react";
@@ -194,6 +195,7 @@ type DownloadingItemProps = Readonly<{
}>;
const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
+ const theme = useTheme();
const [isWaiting, setIsWaiting] = useState(true);
useEffect(() => {
if (giveUpTimeMs === undefined || file.blob !== undefined) {
@@ -211,14 +213,28 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
return (
-
- {file.name}
+
+ {file.name}
+
+
{file.blob ? (
humanBlobSize(file.blob.size)
) : isWaiting ? (
) : (
-
N/A
+
)}
@@ -261,6 +277,20 @@ const styles = {
},
notAvailableText: (theme) => ({
- color: theme.palette.error.main,
+ display: "flex",
+ flexFlow: "row nowrap",
+ alignItems: "center",
+ columnGap: "4px",
+
+ "& > span": {
+ maxHeight: "fit-content",
+ display: "flex",
+ alignItems: "center",
+ color: theme.palette.error.light,
+ },
+
+ "& > p": {
+ opacity: "80%",
+ },
}),
} satisfies Record>;
From 8fb44964aab0a267cd71f65494487b7cbfa02c20 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 13:59:01 +0000
Subject: [PATCH 11/19] fix: resolve linter violations
---
.../WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 34e070c324bf1..89742a263f345 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -1,23 +1,23 @@
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
-import Skeleton from "@mui/material/Skeleton";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
+import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { type FC, useMemo, useState, useRef, useEffect } from "react";
-import { UseQueryOptions, useQueries, useQuery } from "react-query";
+import { type UseQueryOptions, useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
import type {
Workspace,
WorkspaceAgent,
WorkspaceAgentLog,
} from "api/typesGenerated";
+import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
ConfirmDialog,
type ConfirmDialogProps,
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
-import { ErrorAlert } from "components/Alert/ErrorAlert";
const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
@@ -230,7 +230,7 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
) : (
-
+
N/A
From 6935f507edb1d97f3d60f496bc81f0b4e164f43a Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:12:53 +0000
Subject: [PATCH 12/19] fix: update type signature of getErrorDetail
---
site/src/api/errors.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts
index 65f450c644902..621b19856601b 100644
--- a/site/src/api/errors.ts
+++ b/site/src/api/errors.ts
@@ -110,15 +110,18 @@ export const getValidationErrorMessage = (error: unknown): string => {
return validationErrors.map((error) => error.detail).join("\n");
};
-export const getErrorDetail = (error: unknown): string | null => {
+export const getErrorDetail = (error: unknown): string | undefined => {
if (error instanceof Error) {
return "Please check the developer console for more details.";
}
+
if (isApiError(error)) {
- return error.response.data.detail ?? null;
+ return error.response.data.detail;
}
+
if (isApiErrorResponse(error)) {
- return error.detail ?? null;
+ return error.detail;
}
- return null;
+
+ return undefined;
};
From f74eda40723206e6a4addc9ef62787faec23313d Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:14:58 +0000
Subject: [PATCH 13/19] fix: revert log/enabled logic for useAgentLogs
---
.../modules/resources/AgentLogs/useAgentLogs.ts | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
index 8834300713000..eaaa7fdf6469b 100644
--- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts
+++ b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
@@ -29,16 +29,7 @@ export function useAgentLogs(
const queryClient = useQueryClient();
const queryOptions = agentLogs(workspaceId, agentId);
- const query = useQuery({ ...queryOptions, enabled });
-
- // One pitfall with the current approach: the enabled property does NOT
- // prevent the useQuery call above from eventually having data. All it does
- // is prevent it from getting data on its own. Let's say a different useQuery
- // call elsewhere in the app has the same query key and is enabled. When it
- // gets data back from the server, the useQuery call here will re-render with
- // that same new data, even though this state is "disabled". This can EASILY
- // cause bugs.
- const logs = enabled ? query.data : undefined;
+ const { data: logs, isFetched } = useQuery({ ...queryOptions, enabled });
const lastQueriedLogId = useRef(0);
useEffect(() => {
@@ -58,7 +49,7 @@ export function useAgentLogs(
});
useEffect(() => {
- if (agentLifeCycleState !== "starting" || !query.isFetched) {
+ if (agentLifeCycleState !== "starting" || !isFetched) {
return;
}
@@ -85,7 +76,7 @@ export function useAgentLogs(
return () => {
socket.close();
};
- }, [addLogs, agentId, agentLifeCycleState, query.isFetched]);
+ }, [addLogs, agentId, agentLifeCycleState, isFetched]);
return logs;
}
From 7b76eb78addcee55f3d02bcafd924d31307308fa Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:28:48 +0000
Subject: [PATCH 14/19] fix: remove memoization from DownloadLogsDialog
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 82 ++++++++-----------
1 file changed, 33 insertions(+), 49 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 89742a263f345..d11943707b28e 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -4,13 +4,9 @@ import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { type FC, useMemo, useState, useRef, useEffect } from "react";
-import { type UseQueryOptions, useQueries, useQuery } from "react-query";
+import { useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
-import type {
- Workspace,
- WorkspaceAgent,
- WorkspaceAgentLog,
-} from "api/typesGenerated";
+import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
ConfirmDialog,
@@ -46,53 +42,31 @@ export const DownloadLogsDialog: FC = ({
...buildLogs(workspace),
enabled: open,
});
- const buildLogsFile = useMemo(() => {
- return {
- name: `${workspace.name}-build-logs.txt`,
- blob: buildLogsQuery.data
- ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
- type: "text/plain",
- })
- : undefined,
- };
- }, [workspace.name, buildLogsQuery.data]);
-
- // This is clunky, but we have to memoize in two steps to make sure that we
- // don't accidentally break the memo cache every render. We can't tuck
- // everything into a single memo call, because we need to set up React Query
- // state between processing the agents, but we can't violate rules of hooks by
- // putting hooks inside of hooks
- type AgentInfo = Readonly<{
- agents: readonly WorkspaceAgent[];
- logOptionsArray: readonly UseQueryOptions[];
- }>;
-
- const { agents, logOptionsArray } = useMemo(() => {
+
+ const allUniqueAgents = useMemo(() => {
const allAgents = workspace.latest_build.resources.flatMap(
(resource) => resource.agents ?? [],
);
// Can't use the "new Set()" trick because we're not dealing with primitives
- const uniqueAgents = [
- ...new Map(allAgents.map((agent) => [agent.id, agent])).values(),
- ];
-
- return {
- agents: uniqueAgents,
- logOptionsArray: uniqueAgents.map((agent) => {
- return {
- ...agentLogs(workspace.id, agent.id),
- enabled: open,
- };
- }),
- };
- }, [workspace, open]);
-
- const agentLogQueries = useQueries({ queries: logOptionsArray });
- const allFiles = useMemo(() => {
- const files: DownloadableFile[] = [buildLogsFile];
+ const uniqueAgents = new Map(allAgents.map((agent) => [agent.id, agent]));
+ const iterable = [...uniqueAgents.values()];
+ return iterable;
+ }, [workspace.latest_build.resources]);
+
+ const agentLogQueries = useQueries({
+ queries: allUniqueAgents.map((agent) => ({
+ ...agentLogs(workspace.id, agent.id),
+ enabled: open,
+ })),
+ });
- agents.forEach((a, i) => {
+ // Note: trying to memoize this via useMemo got really clunky. Removing all
+ // memoization for now, but if we get to a point where performance matters,
+ // we should make it so that this state doesn't even begin to mount until the
+ // user decides to open the Logs dropdown
+ const allFiles = ((): readonly DownloadableFile[] => {
+ const files = allUniqueAgents.map((a, i) => {
const name = `${a.name}-logs.txt`;
const txt = agentLogQueries[i]?.data?.map((l) => l.output).join("\n");
@@ -101,11 +75,21 @@ export const DownloadLogsDialog: FC = ({
blob = new Blob([txt], { type: "text/plain" });
}
- files.push({ name, blob });
+ return { name, blob };
});
+ const buildLogFile = {
+ name: `${workspace.name}-build-logs.txt`,
+ blob: buildLogsQuery.data
+ ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
+ type: "text/plain",
+ })
+ : undefined,
+ };
+
+ files.unshift(buildLogFile);
return files;
- }, [agentLogQueries, agents, buildLogsFile]);
+ })();
const [isDownloading, setIsDownloading] = useState(false);
const isWorkspaceHealthy = workspace.health.healthy;
From 07ff536463d1a0d682605c0dc6a2409c206db5a9 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:33:23 +0000
Subject: [PATCH 15/19] fix: update name of timeout state
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index d11943707b28e..706d76f40e617 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -15,8 +15,6 @@ import {
import { displayError } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
-const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
-
type DownloadLogsDialogProps = Pick<
ConfirmDialogProps,
"onConfirm" | "onClose" | "open"
@@ -95,10 +93,10 @@ export const DownloadLogsDialog: FC = ({
const isWorkspaceHealthy = workspace.health.healthy;
const isLoadingFiles = allFiles.some((f) => f.blob === undefined);
- const resetDownloadStateIdRef = useRef(undefined);
+ const downloadTimeoutIdRef = useRef(undefined);
useEffect(() => {
const clearTimeoutOnUnmount = () => {
- window.clearTimeout(resetDownloadStateIdRef.current);
+ window.clearTimeout(downloadTimeoutIdRef.current);
};
return clearTimeoutOnUnmount;
@@ -137,7 +135,7 @@ export const DownloadLogsDialog: FC = ({
download(content, `${workspace.name}-logs.zip`);
onClose();
- resetDownloadStateIdRef.current = window.setTimeout(() => {
+ downloadTimeoutIdRef.current = window.setTimeout(() => {
setIsDownloading(false);
}, theme.transitions.duration.leavingScreen);
} catch (error) {
@@ -226,14 +224,16 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
};
function humanBlobSize(size: number) {
+ const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
let i = 0;
while (size > 1024 && i < BLOB_SIZE_UNITS.length) {
size /= 1024;
i++;
}
- // The while condition can break if we accidentally exceed the bounds of the
- // array. Have to be extra sure we have a unit at the very end.
+ // The condition for the while loop above means that over time, we could break
+ // out of the loop because we accidentally went out of the array bounds.
+ // Adding a lot of redundant checks to make sure we always have a usable unit
const finalUnit = BLOB_SIZE_UNITS[i] ?? BLOB_SIZE_UNITS.at(-1) ?? "TB";
return `${size.toFixed(2)} ${finalUnit}`;
}
From 460aa53adbff7a718a8e356bfb1c97f3fb644df6 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:40:16 +0000
Subject: [PATCH 16/19] refactor: make log web sockets logic more clear
---
site/src/modules/resources/AgentLogs/useAgentLogs.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/site/src/modules/resources/AgentLogs/useAgentLogs.ts b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
index eaaa7fdf6469b..943dfcc194396 100644
--- a/site/src/modules/resources/AgentLogs/useAgentLogs.ts
+++ b/site/src/modules/resources/AgentLogs/useAgentLogs.ts
@@ -31,12 +31,18 @@ export function useAgentLogs(
const queryOptions = agentLogs(workspaceId, agentId);
const { data: logs, isFetched } = useQuery({ ...queryOptions, enabled });
+ // Track the ID of the last log received when the initial logs response comes
+ // back. If the logs are not complete, the ID will mark the start point of the
+ // Web sockets response so that the remaining logs can be received over time
const lastQueriedLogId = useRef(0);
useEffect(() => {
- const lastLog = logs?.at(-1);
- const canSetLogId = lastLog !== undefined && lastQueriedLogId.current === 0;
+ const isAlreadyTracking = lastQueriedLogId.current !== 0;
+ if (isAlreadyTracking) {
+ return;
+ }
- if (canSetLogId) {
+ const lastLog = logs?.at(-1);
+ if (lastLog !== undefined) {
lastQueriedLogId.current = lastLog.id;
}
}, [logs]);
From 5a449b42992f0e6a8f1d6a81471dfaf76fc6b431 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 18:43:34 +0000
Subject: [PATCH 17/19] docs: reword comment for clarity
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 706d76f40e617..aa737d16a5399 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -63,7 +63,7 @@ export const DownloadLogsDialog: FC = ({
// memoization for now, but if we get to a point where performance matters,
// we should make it so that this state doesn't even begin to mount until the
// user decides to open the Logs dropdown
- const allFiles = ((): readonly DownloadableFile[] => {
+ const allFiles: readonly DownloadableFile[] = (() => {
const files = allUniqueAgents.map((a, i) => {
const name = `${a.name}-logs.txt`;
const txt = agentLogQueries[i]?.data?.map((l) => l.output).join("\n");
@@ -76,7 +76,7 @@ export const DownloadLogsDialog: FC = ({
return { name, blob };
});
- const buildLogFile = {
+ const buildLogsFile = {
name: `${workspace.name}-build-logs.txt`,
blob: buildLogsQuery.data
? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
@@ -85,7 +85,7 @@ export const DownloadLogsDialog: FC = ({
: undefined,
};
- files.unshift(buildLogFile);
+ files.unshift(buildLogsFile);
return files;
})();
@@ -232,8 +232,9 @@ function humanBlobSize(size: number) {
}
// The condition for the while loop above means that over time, we could break
- // out of the loop because we accidentally went out of the array bounds.
- // Adding a lot of redundant checks to make sure we always have a usable unit
+ // out of the loop because we accidentally shot past the array bounds and i
+ // is at index (BLOB_SIZE_UNITS.length). Adding a lot of redundant checks to
+ // make sure we always have a usable unit
const finalUnit = BLOB_SIZE_UNITS[i] ?? BLOB_SIZE_UNITS.at(-1) ?? "TB";
return `${size.toFixed(2)} ${finalUnit}`;
}
From eb4f88c74149e77ecbc90fc4fe190397a753e293 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 19:21:04 +0000
Subject: [PATCH 18/19] fix: commit current style update progress
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 78 +++++++++++++++----
1 file changed, 61 insertions(+), 17 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index aa737d16a5399..4ea95226ea742 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -1,5 +1,4 @@
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
-import ErrorIcon from "@mui/icons-material/ErrorOutline";
import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
@@ -7,7 +6,7 @@ import { type FC, useMemo, useState, useRef, useEffect } from "react";
import { useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
-import { ErrorAlert } from "components/Alert/ErrorAlert";
+import { Alert } from "components/Alert/Alert";
import {
ConfirmDialog,
type ConfirmDialogProps,
@@ -109,12 +108,7 @@ export const DownloadLogsDialog: FC = ({
hideCancel={false}
title="Download logs"
confirmLoading={isDownloading}
- confirmText={
- <>
- Download
- {!isWorkspaceHealthy && <> {isLoadingFiles ? "partial" : "all"}>}
- >
- }
+ confirmText="Download"
disabled={
isDownloading ||
// If a workspace isn't healthy, let the user download as many logs as
@@ -152,7 +146,9 @@ export const DownloadLogsDialog: FC = ({
{!isWorkspaceHealthy && isLoadingFiles && (
-
+
+ Your workspace is unhealthy. Some logs may be unavailable.
+
)}
@@ -179,6 +175,7 @@ type DownloadingItemProps = Readonly<{
const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
const theme = useTheme();
const [isWaiting, setIsWaiting] = useState(true);
+
useEffect(() => {
if (giveUpTimeMs === undefined || file.blob !== undefined) {
setIsWaiting(true);
@@ -193,6 +190,8 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
return () => window.clearTimeout(timeoutId);
}, [giveUpTimeMs, file]);
+ const { baseName, fileExtension } = extractFileNameInfo(file.name);
+
return (
-
= ({ file, giveUpTimeMs }) => {
!isWaiting && { color: theme.palette.text.disabled },
]}
>
- {file.name}
+
+ {/* {baseName} */}
+ WWWWWWWWWWWWWWWWWWWWWWW
+
+
+ .{fileExtension}
@@ -210,13 +214,7 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
) : isWaiting ? (
) : (
-
+
Not available
)}
@@ -239,6 +237,33 @@ function humanBlobSize(size: number) {
return `${size.toFixed(2)} ${finalUnit}`;
}
+type FileNameInfo = Readonly<{
+ baseName: string;
+ fileExtension: string | undefined;
+}>;
+
+function extractFileNameInfo(filename: string): FileNameInfo {
+ if (filename.length === 0) {
+ return {
+ baseName: "",
+ fileExtension: undefined,
+ };
+ }
+
+ const periodIndex = filename.lastIndexOf(".");
+ if (periodIndex === -1) {
+ return {
+ baseName: filename,
+ fileExtension: undefined,
+ };
+ }
+
+ return {
+ baseName: filename.slice(0, periodIndex),
+ fileExtension: filename.slice(periodIndex + 1),
+ };
+}
+
const styles = {
list: {
listStyle: "none",
@@ -248,17 +273,36 @@ const styles = {
flexDirection: "column",
gap: 8,
},
+
listItem: {
+ width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
+ columnGap: "32px",
},
+
listItemPrimary: (theme) => ({
fontWeight: 500,
color: theme.palette.text.primary,
+ display: "flex",
+ flexFlow: "no nowrap",
}),
+
+ listItemPrimaryBaseName: {
+ minWidth: 0,
+ flexShrink: 1,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+
+ listItemPrimaryFileExtension: {
+ flexShrink: 0,
+ },
+
listItemSecondary: {
fontSize: 14,
+ whiteSpace: "nowrap",
},
notAvailableText: (theme) => ({
From 71692b60e4d3bce65cf57a2de05a02a9d0926ef3 Mon Sep 17 00:00:00 2001
From: Parkreiner
Date: Tue, 2 Jul 2024 19:31:01 +0000
Subject: [PATCH 19/19] fix: finish style updates
---
.../WorkspaceActions/DownloadLogsDialog.tsx | 26 ++++++-------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index 4ea95226ea742..ab1ee817a9de7 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -147,7 +147,8 @@ export const DownloadLogsDialog: FC = ({
{!isWorkspaceHealthy && isLoadingFiles && (
- Your workspace is unhealthy. Some logs may be unavailable.
+ Your workspace is unhealthy. Some logs may be unavailable for
+ download.
)}
@@ -200,11 +201,7 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
!isWaiting && { color: theme.palette.text.disabled },
]}
>
-
- {/* {baseName} */}
- WWWWWWWWWWWWWWWWWWWWWWW
-
-
+ {baseName}
.{fileExtension}
@@ -286,7 +283,9 @@ const styles = {
fontWeight: 500,
color: theme.palette.text.primary,
display: "flex",
- flexFlow: "no nowrap",
+ flexFlow: "row nowrap",
+ columnGap: 0,
+ overflow: "hidden",
}),
listItemPrimaryBaseName: {
@@ -301,6 +300,7 @@ const styles = {
},
listItemSecondary: {
+ flexShrink: 0,
fontSize: 14,
whiteSpace: "nowrap",
},
@@ -310,16 +310,6 @@ const styles = {
flexFlow: "row nowrap",
alignItems: "center",
columnGap: "4px",
-
- "& > span": {
- maxHeight: "fit-content",
- display: "flex",
- alignItems: "center",
- color: theme.palette.error.light,
- },
-
- "& > p": {
- opacity: "80%",
- },
+ color: theme.palette.text.disabled,
}),
} satisfies Record>;