From 0fe820ef220b19eb7f069e5b7d637ab09f363d36 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Mon, 20 Jan 2025 22:22:35 +0100 Subject: [PATCH 01/42] Make docker image configurable with envs --- .dockerignore | 1 - .env | 6 ++++ Dockerfile | 42 ++++++++++++++++++------ bun.lockb | Bin 251224 -> 253008 bytes package.json | 1 + scripts/docker-envs.ts | 28 ++++++++++++++++ scripts/nginx-entrypoint.ts | 57 +++++++++++++++++++++++++++++++++ src/keycloak.ts | 6 ++-- src/layouts/dashboard/Menu.tsx | 15 +++------ src/pages/admin/Admin.tsx | 2 +- src/pages/profile/Profile.tsx | 2 +- 11 files changed, 134 insertions(+), 26 deletions(-) create mode 100755 scripts/docker-envs.ts create mode 100755 scripts/nginx-entrypoint.ts diff --git a/.dockerignore b/.dockerignore index fc06e3e5..814e90a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,6 @@ LICENSE .vscode Makefile helm-charts -.env .editorconfig .idea coverage* diff --git a/.env b/.env index 204f0dcc..d5d71ca8 100644 --- a/.env +++ b/.env @@ -1,3 +1,9 @@ VITE_DEPLOY_API_URL="https://api.cloud.cbh.kth.se/deploy/v2" VITE_ALERT_API_URL="https://alert.app.cloud.cbh.kth.se/" +VITE_KEYCLOAK_URL="https://iam.cloud.cbh.kth.se" +VITE_KEYCLOAK_REALM="cloud" +VITE_KEYCLOAK_CLIENT_ID="landing" +VITE_RANCHER_URL="https://mgmt.cloud.cbh.kth.se" +VITE_DNS_URL="https://dns.cloud.cbh.kth.se" +VITE_MAIA_URL="https://maia.app.cloud.cbh.kth.se/maia" GENERATE_SOURCEMAP=false diff --git a/Dockerfile b/Dockerfile index 656093d2..b9e7f2b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,52 @@ # Build with Bun -FROM docker.io/oven/bun:latest as build +FROM --platform=$BUILDPLATFORM docker.io/oven/bun:latest AS build ARG RELEASE_BRANCH ARG RELEASE_DATE ARG RELEASE_COMMIT +ENV NODE_ENV="production" + +WORKDIR /app + +COPY package*.json bun.lockb ./ + +RUN bun install + +COPY .env . +COPY --chmod=777 scripts/ ./ + +RUN ./docker-envs.ts .env.production && \ + ./nginx-entrypoint.ts && \ + rm .env + ENV VITE_RELEASE_BRANCH=${RELEASE_BRANCH} ENV VITE_RELEASE_DATE=${RELEASE_DATE} ENV VITE_RELEASE_COMMIT=${RELEASE_COMMIT} -ENV VITE_API_URL="https://api.cloud.cbh.kth.se" -ENV VITE_DEPLOY_API_URL="https://api.cloud.cbh.kth.se/deploy/v2" -ENV NODE_ENV="production" +COPY .eslintrc.json jsconfig.json index.html tsconfig*.json vite.config.ts ./ -WORKDIR /app -COPY . /app +COPY . . -RUN bun install RUN bun run build # Serve with NGINX -FROM nginx +FROM nginx:latest COPY --from=build /app/dist /usr/share/nginx/html RUN rm /etc/nginx/conf.d/default.conf COPY nginx/nginx.conf /etc/nginx/conf.d + +COPY --from=build --chmod=777 --link /app/entrypoint.sh . + +# Set default values, can be overriden +ENV DEPLOY_API_URL="https://api.cloud.cbh.kth.se/deploy/v2" +ENV ALERT_API_URL="https://alert.app.cloud.cbh.kth.se/" +ENV KEYCLOAK_URL="https://iam.cloud.cbh.kth.se" +ENV KEYCLOAK_REALM="cloud" +ENV KEYCLOAK_CLIENT_ID="landing" +ENV RANCHER_URL="https://mgmt.cloud.cbh.kth.se" +ENV DNS_URL="https://dns.cloud.cbh.kth.se" +ENV MAIA_URL="https://maia.app.cloud.cbh.kth.se/maia" + EXPOSE 3000 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/bun.lockb b/bun.lockb index 8e46bd01b014d66819e650f34e684ee226d66326..217371b7588d8885231810c763bccf11c0d60831 100755 GIT binary patch delta 38092 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 BEC>Jq delta 36919 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 + fi + + # Find files in the given directory recursively + find "$directory" -type f -exec sed -i "s|{{$old_var}}|$new_value|g" {} + +}\n\n`; + +// Add the updates for all VITE_* env variables +for (const [key, _] of Object.entries(viteEnv)) { + if (key.startsWith("VITE_")) { + const env = key.slice("VITE_".length); + entrypointScript += `update_env_vars_placeholders "/usr/share/nginx/html" "__${env}__" "${env}"\n`; + } +} + +// Start Nginx +entrypointScript += `\n# Start Nginx\nnginx -g 'daemon off;'`; + +console.log(entrypointScript); + +// Get the filename from command-line arguments +const args = process.argv.slice(2); +const outputFilename = args[0] || "entrypoint.sh"; // Default to entrypoint.sh if no argument is provided + +// Define the output file path +const outputFilePath = path.resolve(outputFilename); + +// Write the entrypoint script to the specified file +fs.writeFileSync(outputFilePath, entrypointScript, "utf-8"); + +// Set the file permissions to 777 (executable for all users) +fs.chmodSync(outputFilePath, 0o777); + +console.log( + `${outputFilename} file created with executable permissions (777)!` +); \ No newline at end of file diff --git a/src/keycloak.ts b/src/keycloak.ts index 260f9c25..3b88376c 100644 --- a/src/keycloak.ts +++ b/src/keycloak.ts @@ -1,9 +1,9 @@ import Keycloak from "keycloak-js"; const config = { - url: "https://iam.cloud.cbh.kth.se", - realm: "cloud", - clientId: "landing", + url: import.meta.env.VITE_KEYCLOAK_URL, + realm: import.meta.env.VITE_KEYCLOAK_REALM, + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID, }; const keycloak = new Keycloak(config); diff --git a/src/layouts/dashboard/Menu.tsx b/src/layouts/dashboard/Menu.tsx index 7b9c8465..01cb856c 100644 --- a/src/layouts/dashboard/Menu.tsx +++ b/src/layouts/dashboard/Menu.tsx @@ -153,7 +153,7 @@ export default function Menu() { {t("menu-status")} - {t("menu-cloudstack")} - - {t("menu-rancher")} {t("menu-keycloak")} diff --git a/src/pages/admin/Admin.tsx b/src/pages/admin/Admin.tsx index 1faf3a42..b1cd0fed 100644 --- a/src/pages/admin/Admin.tsx +++ b/src/pages/admin/Admin.tsx @@ -853,7 +853,7 @@ export const Admin = () => { {t("admin-edit-permissions-in") + " "} diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index 43ee6ea6..bcf5839a 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -259,7 +259,7 @@ export function Profile() { + + {loading ? ( + t("loading") + ) : ( + + RTT: + + {" " + lastRefreshRtt + " ms "} + + {t("admin-last-load")}: + + {" " + timeDiffSinceLastRefresh} + + + )} + + + + + + + + + {t("menu-admin-panel")} + + + + {resourceConfig.map((resource, index) => ( + + ))} + + {tabs[activeTab]} + + + + + )} + + ); +} diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 1130c83d..9a923510 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -1 +1 @@ -export { Admin as default } from "./Admin"; +export { default } from "./AdminV2"; From f9ebb325c79f2c41c192c3c39222f5f989da4c30 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 10:59:38 +0100 Subject: [PATCH 04/42] add stale status to new admin page --- src/components/render/Resource.tsx | 30 ++++++++++++++++++++++++++++++ src/locales/en.json | 4 +++- src/locales/se.json | 4 +++- src/pages/admin/AdminV2.tsx | 3 +++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/components/render/Resource.tsx b/src/components/render/Resource.tsx index de21a29d..b0aaa17a 100644 --- a/src/components/render/Resource.tsx +++ b/src/components/render/Resource.tsx @@ -279,3 +279,33 @@ export const renderShared = ( ); }; + +function isOlderThanThreeMonths(accessedAt: string | undefined) { + if (!accessedAt) return false; + + const accessedDate = new Date(accessedAt); + const threeMonthsAgo = new Date(); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + + return accessedDate < threeMonthsAgo; +} + +export const renderStale = ( + row: Resource, + t: TFunction<"translation", undefined> +) => { + const stale = isOlderThanThreeMonths(row?.accessedAt); + if (!stale) return <>; + + return ( + + ); +}; diff --git a/src/locales/en.json b/src/locales/en.json index 43b5fd0e..ca069114 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -538,6 +538,8 @@ "admin-visibility-auth": "Private (auth proxy)", "visibility-public-tooltip": "Your deployment is publicly accessible on the internet.", "visibility-private-tooltip": "Your deployment is private and not reachable on the internet. Only internal traffic is allowed in (traffic between deployments).", - "visibility-auth-tooltip": "Your deployment is private and reachable on the internet through an authentication proxy, this limits the allowed traffic." + "visibility-auth-tooltip": "Your deployment is private and reachable on the internet through an authentication proxy, this limits the allowed traffic.", + "stale": "Stale", + "stale-description": "This resource hasn't been accessed by its owner for more than or equal to 3 months." } } diff --git a/src/locales/se.json b/src/locales/se.json index f1f4e101..6be4e4d4 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -537,6 +537,8 @@ "admin-visibility-auth": "Privat (auth proxy)", "visibility-public-tooltip": "Din deployment är publikt åtkomlig på internet.", "visibility-private-tooltip": "Din deployment är privat och inte åtkomlig på internet. Enbart intern trafik mellan appar är tillåten.", - "visibility-auth-tooltip": "Din deployment är privat och åtkomlig på internet via en autentiserings-proxy, detta begränsar tillåten trafik." + "visibility-auth-tooltip": "Din deployment är privat och åtkomlig på internet via en autentiserings-proxy, detta begränsar tillåten trafik.", + "stale": "Föråldrad", + "stale-description": "Denna resurs har inte åtkommits av dess ägare under mer än eller lika med 3 månader." } } diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index 08fe01ed..2bb66e8d 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -35,6 +35,7 @@ import { renderResourceStatus, renderResourceWithGPU, renderShared, + renderStale, renderStatusCode, } from "../../components/render/Resource"; import { Resource, Uuid } from "../../types"; @@ -122,6 +123,7 @@ export default function AdminV2() { {renderResourceStatus(deployment as Resource, t)} {renderStatusCode(deployment as Resource)} {renderShared(deployment as Resource, t)} + {renderStale(deployment as Resource, t)} ); }, @@ -182,6 +184,7 @@ export default function AdminV2() { {renderResourceStatus(vm as Resource, t)} {renderShared(vm as Resource, t)} + {renderStale(vm as Resource, t)} ); }, From de676c1423382ce3cf633d4af344b8210899bbf1 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 11:01:25 +0100 Subject: [PATCH 05/42] add stale status to deploy page --- src/pages/deploy/Deploy.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/deploy/Deploy.tsx b/src/pages/deploy/Deploy.tsx index eff998c2..564aa1b9 100644 --- a/src/pages/deploy/Deploy.tsx +++ b/src/pages/deploy/Deploy.tsx @@ -46,6 +46,7 @@ import { ThemeColor } from "../../theme/types"; import { deleteVM } from "../../api/deploy/vms"; import { AlertList } from "../../components/AlertList"; import { NoWrapTable as Table } from "../../components/NoWrapTable"; +import { renderStale } from "../../components/render/Resource"; const descendingComparator = ( a: Record, @@ -542,6 +543,7 @@ export function Deploy() { {renderResourceStatus(row)} {renderStatusCode(row)} {renderShared(row)} + {renderStale(row, t)} From 072cbfcc56c7b87bafe2c26c1676d890885c5ade Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 18:57:14 +0100 Subject: [PATCH 06/42] add stale header to deployments in edit mode --- src/components/admin/ResourceTab.tsx | 39 ++++++---- src/components/admin/SearchBar.tsx | 91 ++++++++++++++++++++++ src/components/admin/SearchFilterPopup.tsx | 91 ++++++++++++++++++++++ src/components/admin/TimeAgo.tsx | 50 ++++++++++++ src/components/admin/searchTypes.ts | 13 ++++ src/components/render/Resource.tsx | 2 +- src/locales/en.json | 4 +- src/locales/se.json | 6 +- src/pages/admin/AdminV2.tsx | 36 +++++++++ src/pages/edit/Edit.tsx | 41 ++++++++++ 10 files changed, 352 insertions(+), 21 deletions(-) create mode 100644 src/components/admin/SearchBar.tsx create mode 100644 src/components/admin/SearchFilterPopup.tsx create mode 100644 src/components/admin/TimeAgo.tsx create mode 100644 src/components/admin/searchTypes.ts diff --git a/src/components/admin/ResourceTab.tsx b/src/components/admin/ResourceTab.tsx index a73ca236..cb4903a7 100644 --- a/src/components/admin/ResourceTab.tsx +++ b/src/components/admin/ResourceTab.tsx @@ -13,17 +13,16 @@ import { Box, Button, TablePagination, - InputAdornment, SxProps, Theme, } from "@mui/material"; import { useTranslation } from "react-i18next"; import ConfirmButton from "../ConfirmButton"; -import { SearchStyle } from "./SearchStyle"; -import Iconify from "../Iconify"; +import SearchBar from "./SearchBar"; +import { Category, QueryModifier } from "./searchTypes"; interface ResourceTabProps { - resourceName: string; + resourceName: String; data: T[] | undefined; filteredData: T[] | undefined; filter: string | undefined; @@ -40,6 +39,10 @@ interface ResourceTabProps { withConfirm?: boolean; }[]; OnClickModal?: React.ComponentType<{ data: T }>; + category?: Category; + setCategory?: Dispatch>; + queryModifier?: QueryModifier[Category]; + setQueryModifier?: Dispatch>; } const ResourceTab = ({ @@ -51,6 +54,10 @@ const ResourceTab = ({ columns, actions, OnClickModal, + category, + setCategory, + queryModifier, + setQueryModifier, }: ResourceTabProps) => { const { t } = useTranslation(); const loading = !data; @@ -95,7 +102,8 @@ const ResourceTab = ({ page * rowsPerPage + rowsPerPage ); - const modalBoxStyles: SxProps = { + // annoying TS compile issue for some reason + const modalBoxStyles: any = { position: "absolute" as const, top: "50%" as const, left: "50%" as const, @@ -114,18 +122,15 @@ const ResourceTab = ({ {resourceName} - setFilter(e.target.value)} - placeholder={t("deploy-search")} - startAdornment={ - - - - } + {}} + category={category} + setCategory={setCategory} + queryModifier={queryModifier} + setQueryModifier={setQueryModifier} /> {loading ? ( diff --git a/src/components/admin/SearchBar.tsx b/src/components/admin/SearchBar.tsx new file mode 100644 index 00000000..d87e2996 --- /dev/null +++ b/src/components/admin/SearchBar.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import { + TextField, + InputAdornment, + Stack, + IconButton, + Tooltip, +} from "@mui/material"; +import SearchFilterPopup from "./SearchFilterPopup"; +import { Search, Sort } from "@mui/icons-material"; +import { Category, QueryModifier } from "./searchTypes"; + +type SearchBarProps = { + searchText?: string; + searchQuery: string; + setSearchQuery: (value: string) => void; + category?: Category; + queryModifier?: QueryModifier[Category]; + setCategory?: (value: Category) => void; + setQueryModifier?: (value: QueryModifier[Category]) => void; + onSearch: () => void; +}; + +const SearchBar: React.FC = ({ + searchText = "Search", + searchQuery, + setSearchQuery, + category, + queryModifier, + setCategory, + setQueryModifier, + onSearch, +}) => { + const [popupAnchor, setPopupAnchor] = useState(null); + + const handleFilterClick = (event: React.MouseEvent) => { + setPopupAnchor(popupAnchor ? null : event.currentTarget); // Toggle popup + }; + + const handleClosePopup = () => { + setPopupAnchor(null); + }; + + const renderFilter = + category != undefined && + setCategory != undefined && + queryModifier != undefined && + setQueryModifier != undefined; + + return ( + <> + + setSearchQuery(e.target.value)} + InputProps={{ + endAdornment: ( + + {renderFilter && ( + + + + + + )} + + + + + ), + }} + /> + + {renderFilter && ( + + )} + + ); +}; + +export default SearchBar; diff --git a/src/components/admin/SearchFilterPopup.tsx b/src/components/admin/SearchFilterPopup.tsx new file mode 100644 index 00000000..6da711ed --- /dev/null +++ b/src/components/admin/SearchFilterPopup.tsx @@ -0,0 +1,91 @@ +import { + Popper, + Paper, + ClickAwayListener, + Typography, + MenuItem, + MenuList, + Stack, + Divider, +} from "@mui/material"; +import { Category, QueryModifier } from "./searchTypes"; + +type SearchFilterPopupProps = { + anchorEl: HTMLElement | null; + onClose: () => void; + category: Category; + setCategory: (value: Category) => void; + queryModifier: QueryModifier[Category]; + setQueryModifier: (value: QueryModifier[Category]) => void; +}; + +export default function SearchFilterPopup({ + anchorEl, + onClose, + category, + setCategory, + queryModifier, + setQueryModifier, +}: SearchFilterPopupProps) { + const isOpen = Boolean(anchorEl); + + // Category options + const categoryOptions: Category[] = ["Matches", "User", "Attribute"]; + + // Query modifier options based on category + const queryModifierOptions: Record = { + Matches: [], + User: ["owns", "hasAccess"], + Attribute: ["resourceAttribute"], + }; + + return ( + + + + + {/* Category Selection */} +
+ Category + + {categoryOptions.map((option) => ( + setCategory(option)} + > + {option} + + ))} + +
+ + + {/* Query Modifier Selection */} + {(queryModifierOptions[category] || []).length > 0 && ( + <> +
+ Query Modifier + + {(queryModifierOptions[category] || []).map((option) => ( + + setQueryModifier(option as QueryModifier[Category]) + } + > + {option} + + ))} + +
+ + + )} +
+
+
+
+ ); +} diff --git a/src/components/admin/TimeAgo.tsx b/src/components/admin/TimeAgo.tsx new file mode 100644 index 00000000..789e47aa --- /dev/null +++ b/src/components/admin/TimeAgo.tsx @@ -0,0 +1,50 @@ +import React, { useState, useEffect } from "react"; +import { Typography } from "@mui/material"; + +const TimeAgo: React.FC<{ createdAt: string | undefined }> = ({ + createdAt, +}) => { + const [timeAgo, setTimeAgo] = useState(""); + + const calculateTimeAgo = (createdAt: string) => { + const now = new Date().getTime(); + const createdDate = new Date(createdAt).getTime(); + const diff = now - createdDate; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days} day${days > 1 ? "s" : ""} ago`; + } + if (hours > 0) { + return `${hours} hour${hours > 1 ? "s" : ""} ago`; + } + if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; + } + if (seconds > 0) { + return `${seconds} second${seconds > 1 ? "s" : ""} ago`; + } + return "Just now"; + }; + + useEffect(() => { + if (createdAt) { + setTimeAgo(calculateTimeAgo(createdAt)); + + const interval = setInterval(() => { + if (createdAt) { + setTimeAgo(calculateTimeAgo(createdAt)); + } + }, 1000); + + return () => clearInterval(interval); // Clean up the interval on component unmount + } + }, [createdAt]); + + return {timeAgo}; +}; + +export default TimeAgo; diff --git a/src/components/admin/searchTypes.ts b/src/components/admin/searchTypes.ts new file mode 100644 index 00000000..c976cade --- /dev/null +++ b/src/components/admin/searchTypes.ts @@ -0,0 +1,13 @@ +export type Category = "Matches"|"User" | "Attribute"; + +export type QueryModifier = { + Matches: ""; + User: "owns" | "hasAccess"; + Attribute: "resourceAttribute"; +}; + +export type QueryParameters = { + matches: string; + byUserName: string; + resourceAttribute: "stale" | "shared"; +}; \ No newline at end of file diff --git a/src/components/render/Resource.tsx b/src/components/render/Resource.tsx index b0aaa17a..8f019c07 100644 --- a/src/components/render/Resource.tsx +++ b/src/components/render/Resource.tsx @@ -280,7 +280,7 @@ export const renderShared = ( ); }; -function isOlderThanThreeMonths(accessedAt: string | undefined) { +export function isOlderThanThreeMonths(accessedAt: string | undefined) { if (!accessedAt) return false; const accessedDate = new Date(accessedAt); diff --git a/src/locales/en.json b/src/locales/en.json index ca069114..af99f9f7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -540,6 +540,8 @@ "visibility-private-tooltip": "Your deployment is private and not reachable on the internet. Only internal traffic is allowed in (traffic between deployments).", "visibility-auth-tooltip": "Your deployment is private and reachable on the internet through an authentication proxy, this limits the allowed traffic.", "stale": "Stale", - "stale-description": "This resource hasn't been accessed by its owner for more than or equal to 3 months." + "stale-description": "This resource hasn't been accessed by its owner for more than or equal to 3 months.", + "stale-and-disabled-description": "This resource has been disabled due to inactivity. Please contact the resource owner to log in and restore access.", + "stale-and-not-disabled-description": "Due to prolonged inactivity, this resource will be disabled soon." } } diff --git a/src/locales/se.json b/src/locales/se.json index 6be4e4d4..7205ce55 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -538,7 +538,9 @@ "visibility-public-tooltip": "Din deployment är publikt åtkomlig på internet.", "visibility-private-tooltip": "Din deployment är privat och inte åtkomlig på internet. Enbart intern trafik mellan appar är tillåten.", "visibility-auth-tooltip": "Din deployment är privat och åtkomlig på internet via en autentiserings-proxy, detta begränsar tillåten trafik.", - "stale": "Föråldrad", - "stale-description": "Denna resurs har inte åtkommits av dess ägare under mer än eller lika med 3 månader." + "stale": "Inaktiv", + "stale-description": "Denna resurs har inte åtkommits av dess ägare under mer än eller lika med 3 månader.", + "stale-and-disabled-description": "Denna resurs har inaktiverats på grund av inaktivitet. Kontakta resursägaren för att logga in och återställa åtkomsten.", + "stale-and-not-disabled-description": "På grund av långvarig inaktivitet kommer denna resurs att inaktiveras inom en snar framtid." } } diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index 2bb66e8d..073fe3c7 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -39,6 +39,8 @@ import { renderStatusCode, } from "../../components/render/Resource"; import { Resource, Uuid } from "../../types"; +import TimeAgo from "../../components/admin/TimeAgo"; +import { Category, QueryModifier } from "../../components/admin/searchTypes"; export default function AdminV2() { const { t } = useTranslation(); @@ -84,6 +86,11 @@ export default function AdminV2() { const navigate = useNavigate(); const [activeTab, setActiveTab] = useState(0); + const [categoryTemp, setCategoryTemp] = useState( + "Matches" + ); + const [queryModifierTemp, setQueryModifierTemp] = + useState(""); const handleChangeTab = (_: any, newTab: number) => { setActiveTab(newTab); @@ -333,6 +340,31 @@ export default function AdminV2() { }, { label: "Jobs", + columns: [ + { id: "id", label: "ID" }, + { + id: "userId", + label: "User", + renderFunc: (userId: string) => { + return users?.find((user) => user.id === userId)?.username; + }, + }, + { + id: "type", + label: "Type", + }, + { + id: "status", + label: "Status", + }, + { + id: "createdAt", + label: "Created", + renderFunc: (createdAt: string | undefined) => + TimeAgo({ createdAt: createdAt }), + }, + { id: "runAfter", label: "Run After" }, + ], }, ]; @@ -412,6 +444,10 @@ export default function AdminV2() { setFilter={resourceLookup[index].setFilter} columns={config.columns} actions={config.actions} + category={categoryTemp} + setCategory={setCategoryTemp} + queryModifier={queryModifierTemp} + setQueryModifier={setQueryModifierTemp} /> )); diff --git a/src/pages/edit/Edit.tsx b/src/pages/edit/Edit.tsx index d023d6a1..21b510b1 100644 --- a/src/pages/edit/Edit.tsx +++ b/src/pages/edit/Edit.tsx @@ -48,6 +48,8 @@ import { AlertList } from "../../components/AlertList"; import { Specs } from "./Specs"; import { ReplicaStatus } from "./deployments/ReplicaStatus"; import ProxyManager from "./vms/ProxyManager"; +import { isOlderThanThreeMonths } from "../../components/render/Resource"; +import Label from "../../components/Label"; export function Edit() { const { t } = useTranslation(); @@ -192,6 +194,44 @@ export function Edit() { /> ); }; + const renderStaleResourceHeaderFullWidth = (resource: Resource) => { + const stale = isOlderThanThreeMonths(resource?.accessedAt); + if (!stale) return <>; + + return ( + + ); + }; return ( <> @@ -330,6 +370,7 @@ export function Edit() { + {renderStaleResourceHeaderFullWidth(resource)} {resource.type === "vm" && } From 553b3df996c8ae055c1612c21a73b8719333b5ad Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 20:26:36 +0100 Subject: [PATCH 07/42] fix fomatting + add translations --- src/components/admin/ResourceTab.tsx | 38 ++++++++++----------- src/components/admin/searchTypes.ts | 4 +-- src/components/render/Resource.tsx | 37 +++++++++++++++++---- src/locales/en.json | 6 +++- src/locales/se.json | 6 +++- src/pages/admin/AdminV2.tsx | 4 +-- src/pages/edit/Edit.tsx | 49 +++++++++++++++++++++------- src/utils/staleDates.ts | 27 +++++++++++++++ 8 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 src/utils/staleDates.ts diff --git a/src/components/admin/ResourceTab.tsx b/src/components/admin/ResourceTab.tsx index cb4903a7..a7d92387 100644 --- a/src/components/admin/ResourceTab.tsx +++ b/src/components/admin/ResourceTab.tsx @@ -10,16 +10,15 @@ import { Skeleton, Typography, Modal, - Box, Button, TablePagination, - SxProps, - Theme, + useTheme, } from "@mui/material"; import { useTranslation } from "react-i18next"; import ConfirmButton from "../ConfirmButton"; import SearchBar from "./SearchBar"; import { Category, QueryModifier } from "./searchTypes"; +import { CustomTheme } from "../../theme/types"; interface ResourceTabProps { resourceName: String; @@ -60,6 +59,7 @@ const ResourceTab = ({ setQueryModifier, }: ResourceTabProps) => { const { t } = useTranslation(); + const theme = useTheme(); const loading = !data; const resolvedColumns = columns && columns.length > 0 @@ -103,18 +103,18 @@ const ResourceTab = ({ ); // annoying TS compile issue for some reason - const modalBoxStyles: any = { - position: "absolute" as const, - top: "50%" as const, - left: "50%" as const, - transform: "translate(-50%, -50%)" as const, - width: "80vw" as const, - height: "80vh" as const, - bgcolor: "background.paper" as const, - boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.1)" as const, - p: "1em" as const, - borderRadius: "2em" as const, - overflow: "auto" as const, + const modalBoxStyles: React.CSSProperties = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "80vw", + height: "80vh", + backgroundColor: `${(theme as CustomTheme).palette.grey[500_32]} !important`, + boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.1)", + padding: "1em", + borderRadius: "2em", + overflow: "auto", }; return ( @@ -228,9 +228,9 @@ const ResourceTab = ({ setSelectedItem(undefined); }} > - +
- +
) : ( selectedItem && ( @@ -240,7 +240,7 @@ const ResourceTab = ({ setSelectedItem(undefined); }} > - +
Item Details @@ -262,7 +262,7 @@ const ResourceTab = ({ ) : ( No item selected. )} - +
) )} diff --git a/src/components/admin/searchTypes.ts b/src/components/admin/searchTypes.ts index c976cade..68db1690 100644 --- a/src/components/admin/searchTypes.ts +++ b/src/components/admin/searchTypes.ts @@ -1,4 +1,4 @@ -export type Category = "Matches"|"User" | "Attribute"; +export type Category = "Matches" | "User" | "Attribute"; export type QueryModifier = { Matches: ""; @@ -10,4 +10,4 @@ export type QueryParameters = { matches: string; byUserName: string; resourceAttribute: "stale" | "shared"; -}; \ No newline at end of file +}; diff --git a/src/components/render/Resource.tsx b/src/components/render/Resource.tsx index 8f019c07..c400ac17 100644 --- a/src/components/render/Resource.tsx +++ b/src/components/render/Resource.tsx @@ -11,6 +11,7 @@ import { ThemeColor } from "../../theme/types"; import { sentenceCase } from "change-case"; import { getReasonPhrase } from "http-status-codes"; import { TFunction } from "i18next"; +import { getDaysLeftUntilStale } from "../../utils/staleDates"; export const renderResourceButtons = (resource: Resource) => { if ( @@ -294,17 +295,41 @@ export const renderStale = ( row: Resource, t: TFunction<"translation", undefined> ) => { - const stale = isOlderThanThreeMonths(row?.accessedAt); - if (!stale) return <>; + const warningDaysBeforeStale = 30; + const daysLeftUntilStale = getDaysLeftUntilStale(row?.accessedAt); + const stale = + typeof daysLeftUntilStale === "number" + ? daysLeftUntilStale <= 0 + : isOlderThanThreeMonths(row?.accessedAt); + + if ( + !stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale) + ) + return <>; return ( ); diff --git a/src/locales/en.json b/src/locales/en.json index af99f9f7..aae77e04 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -542,6 +542,10 @@ "stale": "Stale", "stale-description": "This resource hasn't been accessed by its owner for more than or equal to 3 months.", "stale-and-disabled-description": "This resource has been disabled due to inactivity. Please contact the resource owner to log in and restore access.", - "stale-and-not-disabled-description": "Due to prolonged inactivity, this resource will be disabled soon." + "stale-and-not-disabled-description": "Due to prolonged inactivity, this resource will be disabled soon.", + "stale-soon": "Stale soon", + "stale-soon-description": "This resource will become stale soon due to inactivity from the owner", + "stale-in": "Stale in", + "days-left": "days left" } } diff --git a/src/locales/se.json b/src/locales/se.json index 7205ce55..5865aec3 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -541,6 +541,10 @@ "stale": "Inaktiv", "stale-description": "Denna resurs har inte åtkommits av dess ägare under mer än eller lika med 3 månader.", "stale-and-disabled-description": "Denna resurs har inaktiverats på grund av inaktivitet. Kontakta resursägaren för att logga in och återställa åtkomsten.", - "stale-and-not-disabled-description": "På grund av långvarig inaktivitet kommer denna resurs att inaktiveras inom en snar framtid." + "stale-and-not-disabled-description": "På grund av långvarig inaktivitet kommer denna resurs att inaktiveras inom en snar framtid.", + "stale-soon": "Snart inaktiv", + "stale-soon-description": "Denna resurs kommer snart att bli avaktiverad på grund av ägarens inaktivitet", + "stale-in": "Inaktiv om", + "days-left": "dagar kvar" } } diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index 073fe3c7..c5ec48fe 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -275,7 +275,7 @@ export default function AdminV2() { }; return ( - +
CPU - +
); }, }, diff --git a/src/pages/edit/Edit.tsx b/src/pages/edit/Edit.tsx index 21b510b1..d746d0f0 100644 --- a/src/pages/edit/Edit.tsx +++ b/src/pages/edit/Edit.tsx @@ -50,6 +50,7 @@ import { ReplicaStatus } from "./deployments/ReplicaStatus"; import ProxyManager from "./vms/ProxyManager"; import { isOlderThanThreeMonths } from "../../components/render/Resource"; import Label from "../../components/Label"; +import { getDaysLeftUntilStale } from "../../utils/staleDates"; export function Edit() { const { t } = useTranslation(); @@ -195,13 +196,31 @@ export function Edit() { ); }; const renderStaleResourceHeaderFullWidth = (resource: Resource) => { - const stale = isOlderThanThreeMonths(resource?.accessedAt); - if (!stale) return <>; + const warningDaysBeforeStale = 30; + const daysLeftUntilStale = getDaysLeftUntilStale(resource?.accessedAt); + const stale = + typeof daysLeftUntilStale === "number" + ? daysLeftUntilStale <= 0 + : isOlderThanThreeMonths(resource?.accessedAt); + + if ( + !stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale) + ) + return <>; + + // Styles for the icon and header + const boxStyles: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: "8px", + }; return ( ); diff --git a/src/utils/staleDates.ts b/src/utils/staleDates.ts new file mode 100644 index 00000000..c3a6b77d --- /dev/null +++ b/src/utils/staleDates.ts @@ -0,0 +1,27 @@ +export function getDaysLeftUntilStale( + accessedAt: string | undefined +): number | false { + if (!accessedAt) return false; + + const accessedDate = new Date(accessedAt); + const staleDate = new Date(accessedDate); + staleDate.setMonth(staleDate.getMonth() + 3); + + const today = new Date(); + const timeDifference = staleDate.getTime() - today.getTime(); + const daysLeft = Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); + + return daysLeft > 0 ? daysLeft : 0; +} + +export function getStaleTimestamp( + accessedAt: string | undefined +): number | false { + if (!accessedAt) return false; + + const accessedDate = new Date(accessedAt); + const staleDate = new Date(accessedDate); + staleDate.setMonth(staleDate.getMonth() + 3); + + return staleDate.getTime(); +} From f6a28287a3ee80575487955560698c29ec683beb Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 20:33:20 +0100 Subject: [PATCH 08/42] fix stale-and-not-disabled on vms --- src/pages/edit/Edit.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/edit/Edit.tsx b/src/pages/edit/Edit.tsx index d746d0f0..556f509a 100644 --- a/src/pages/edit/Edit.tsx +++ b/src/pages/edit/Edit.tsx @@ -250,9 +250,10 @@ export function Edit() { > {stale ? t("stale-description") + - (resource.status === "resourceDisabled" + (resource.status === "resourceDisabled" || + resource.status === "resourceStopped" ? " " + t("stale-and-disabled-description") - : " " + t("stale-and-not-disabled")) + : " " + t("stale-and-not-disabled-description")) : t("stale-soon-description") + ` (${daysLeftUntilStale} ${t("days-left")})`}
From 29cc886fa0e39ea42faa53bbf6e2d3a4e4f2b755 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 20:52:57 +0100 Subject: [PATCH 09/42] untrack admin page refactoring to commit stale labels and headers --- src/App.tsx | 5 +---- src/pages/admin/index.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 42389081..21597977 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import { IconButton } from "@mui/material"; import Iconify from "./components/Iconify"; import { ThemeModeContextProvider } from "./contexts/ThemeModeContext"; import { AlertContextProvider } from "./contexts/AlertContext"; -import { AdminResourceContextProvider } from "./contexts/AdminResourceContext"; export default function App() { return ( @@ -40,9 +39,7 @@ export default function App() { - - - + diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 9a923510..1130c83d 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -1 +1 @@ -export { default } from "./AdminV2"; +export { Admin as default } from "./Admin"; From 0661df0a25ff642b00ce8e3b5f93797c556af410 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 16:45:55 +0100 Subject: [PATCH 10/42] fix bug when fetching a deployment by id --- src/api/deploy/deployments.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/deploy/deployments.ts b/src/api/deploy/deployments.ts index 28923cd6..610aa929 100644 --- a/src/api/deploy/deployments.ts +++ b/src/api/deploy/deployments.ts @@ -9,7 +9,11 @@ export const getDeployment = async (token: string, id: string) => { }, }); const response = [await res.json()]; - const result = response.map((obj) => ({ ...obj, type: "deployment" })); + const result = response.map((obj) => ({ + ...obj, + deploymentType: obj.type, + type: "deployment", + })); if (Array.isArray(result)) return result; else throw new Error("Error getting deployments, response was not an array"); }; From 046811fa458bce151eb8fe705ba9609c9efc7884 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 18:52:18 +0100 Subject: [PATCH 11/42] Add option for admins to make deployments and VMs always active --- bun.lockb | Bin 253008 -> 253008 bytes package.json | 2 +- src/components/render/Resource.tsx | 7 ++- src/locales/en.json | 5 +- src/locales/se.json | 5 +- src/pages/edit/Edit.tsx | 10 +++- src/pages/edit/NeverStaleMode.tsx | 90 +++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 src/pages/edit/NeverStaleMode.tsx diff --git a/bun.lockb b/bun.lockb index 217371b7588d8885231810c763bccf11c0d60831..ecabe3ddd2abd4522291cd6a7127a797d62ab64a 100755 GIT binary patch delta 152 zcmV;J0B8TuxDU{{50EY(IfNL_oxa>*(Xn~^X$v&%Yt>i+`kRcWq9QZ&j`N+Tu}&&* zlXN&JlOS0Mv#D^5-9Qk*nW4S-jLN)2*a*-SB5JR@kk_Q0mGDP^^Z-)lt$=nDcHpdW zs$gRf!$JyeejaWyg)m?Z>51XZ6Wg^mq1=acP64-dP6GQs0W-I8jslD1mmx0$Ah#TI G12k#r(MX{H delta 154 zcmV;L0A>HsxDU{{50EY(S5--JZ>vXSamE*+x&(Z!_s<)%P~>WdNv)HR&DHdRu}&&* z0Th#ACnu92SqZbLaE#qRGodzyY4K$!D<6e_Xh=2>8_W!4B*e*anWh9idCF>;$Vy~j z1fj-|2={j diff --git a/package.json b/package.json index 272a3471..edc30538 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@iconify/react": "^4.1.1", - "@kthcloud/go-deploy-types": "^1.0.20", + "@kthcloud/go-deploy-types": "^1.0.23", "@mui/icons-material": "^5.15.20", "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.20", diff --git a/src/components/render/Resource.tsx b/src/components/render/Resource.tsx index c400ac17..fd582e5b 100644 --- a/src/components/render/Resource.tsx +++ b/src/components/render/Resource.tsx @@ -303,9 +303,10 @@ export const renderStale = ( : isOlderThanThreeMonths(row?.accessedAt); if ( - !stale && - (daysLeftUntilStale === false || - (daysLeftUntilStale as number) > warningDaysBeforeStale) + row.neverStale || + (!stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale)) ) return <>; diff --git a/src/locales/en.json b/src/locales/en.json index aae77e04..ea4b76db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -546,6 +546,9 @@ "stale-soon": "Stale soon", "stale-soon-description": "This resource will become stale soon due to inactivity from the owner", "stale-in": "Stale in", - "days-left": "days left" + "days-left": "days left", + "never-stale-option-header": "Always Active Option", + "never-stale-option-header-subheader": "Control whether this resource should remain active despite inactivity.", + "never-stale": "Always Active" } } diff --git a/src/locales/se.json b/src/locales/se.json index 5865aec3..a307c25f 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -545,6 +545,9 @@ "stale-soon": "Snart inaktiv", "stale-soon-description": "Denna resurs kommer snart att bli avaktiverad på grund av ägarens inaktivitet", "stale-in": "Inaktiv om", - "days-left": "dagar kvar" + "days-left": "dagar kvar", + "never-stale-option-header": "Aldrig inaktiv-alternativ", + "never-stale-option-header-subheader": "Styr om denna resurs ska förbli aktiv trots inaktivitet.", + "never-stale": "Alltid aktiv" } } diff --git a/src/pages/edit/Edit.tsx b/src/pages/edit/Edit.tsx index 556f509a..ad824890 100644 --- a/src/pages/edit/Edit.tsx +++ b/src/pages/edit/Edit.tsx @@ -51,6 +51,7 @@ import ProxyManager from "./vms/ProxyManager"; import { isOlderThanThreeMonths } from "../../components/render/Resource"; import Label from "../../components/Label"; import { getDaysLeftUntilStale } from "../../utils/staleDates"; +import NeverStaleMode from "./NeverStaleMode"; export function Edit() { const { t } = useTranslation(); @@ -204,9 +205,10 @@ export function Edit() { : isOlderThanThreeMonths(resource?.accessedAt); if ( - !stale && - (daysLeftUntilStale === false || - (daysLeftUntilStale as number) > warningDaysBeforeStale) + resource.neverStale || + (!stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale)) ) return <>; @@ -449,6 +451,8 @@ export function Edit() { )} + {user?.admin && } + diff --git a/src/pages/edit/NeverStaleMode.tsx b/src/pages/edit/NeverStaleMode.tsx new file mode 100644 index 00000000..7af52466 --- /dev/null +++ b/src/pages/edit/NeverStaleMode.tsx @@ -0,0 +1,90 @@ +import { + Card, + CardContent, + CardHeader, + FormControlLabel, + Switch, +} from "@mui/material"; +import { useKeycloak } from "@react-keycloak/web"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Job, Resource } from "../../types"; +import { enqueueSnackbar } from "notistack"; +import { updateDeployment } from "../../api/deploy/deployments"; +import { updateVM } from "../../api/deploy/vms"; +import useResource from "../../hooks/useResource"; +import { errorHandler } from "../../utils/errorHandler"; + +export default function NeverStaleMode({ resource }: { resource: Resource }) { + const { t } = useTranslation(); + const { keycloak, initialized } = useKeycloak(); + const { queueJob, beginFastLoad } = useResource(); + + const [neverStale, setNeverStale] = useState( + resource?.neverStale || false + ); + const [isUpdating, setIsUpdating] = useState(false); + useEffect(() => { + if (!isUpdating) setNeverStale(resource?.neverStale || false); + }, [resource, isUpdating]); + + const handleNeverStaleChange = async (neverStaleV: boolean) => { + if (!(initialized && resource && keycloak.token)) { + enqueueSnackbar(t("error-updating"), { variant: "error" }); + return; + } + + try { + let result: Job | null = null; + if (resource.type === "deployment") { + result = await updateDeployment( + resource.id, + { neverStale: neverStaleV }, + keycloak.token + ); + } else if (resource.type === "vm") { + result = await updateVM(keycloak.token, resource.id, { + //@ts-ignore not added yet + neverStale: neverStaleV, + }); + } + + if (result) { + queueJob(result); + beginFastLoad(); + enqueueSnackbar(t("saving-never-stale"), { variant: "info" }); + setNeverStale(neverStaleV); + setIsUpdating(true); + } + } catch (error: any) { + errorHandler(error).forEach((e) => + enqueueSnackbar(t("error-updating") + ": " + e, { + variant: "error", + }) + ); + } finally { + setIsUpdating(false); + } + }; + + return ( + + + + handleNeverStaleChange(e.target.checked)} + inputProps={{ "aria-label": "controlled" }} + /> + } + label={t("never-stale")} + /> + + + ); +} From da36ea259a9ef441d09bff6cafae52e4ab3d12e1 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 18:55:05 +0100 Subject: [PATCH 12/42] fix tsc --- src/pages/create/CreateVm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/create/CreateVm.tsx b/src/pages/create/CreateVm.tsx index 414e73dd..7a29bf1f 100644 --- a/src/pages/create/CreateVm.tsx +++ b/src/pages/create/CreateVm.tsx @@ -169,6 +169,7 @@ export default function CreateVm({ diskSize: diskSize, ram: ram, ports: [], + neverStale: false, }; // zone: selectedZone, From ce68aaabfd7054de25b0bc51ee6a785b7f4f822d Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 16:45:55 +0100 Subject: [PATCH 13/42] fix bug when fetching a deployment by id --- src/api/deploy/deployments.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/deploy/deployments.ts b/src/api/deploy/deployments.ts index 28923cd6..610aa929 100644 --- a/src/api/deploy/deployments.ts +++ b/src/api/deploy/deployments.ts @@ -9,7 +9,11 @@ export const getDeployment = async (token: string, id: string) => { }, }); const response = [await res.json()]; - const result = response.map((obj) => ({ ...obj, type: "deployment" })); + const result = response.map((obj) => ({ + ...obj, + deploymentType: obj.type, + type: "deployment", + })); if (Array.isArray(result)) return result; else throw new Error("Error getting deployments, response was not an array"); }; From 057707fbe60700c1b92240ea2b4699d80f2eceab Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 22 Jan 2025 20:52:57 +0100 Subject: [PATCH 14/42] untrack admin page refactoring to commit stale labels and headers --- src/App.tsx | 5 +---- src/pages/admin/index.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 42389081..21597977 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import { IconButton } from "@mui/material"; import Iconify from "./components/Iconify"; import { ThemeModeContextProvider } from "./contexts/ThemeModeContext"; import { AlertContextProvider } from "./contexts/AlertContext"; -import { AdminResourceContextProvider } from "./contexts/AdminResourceContext"; export default function App() { return ( @@ -40,9 +39,7 @@ export default function App() { - - - + diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 9a923510..1130c83d 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -1 +1 @@ -export { default } from "./AdminV2"; +export { Admin as default } from "./Admin"; From 1c587aeedf35061b4e4d7ba35511554b19c7f8d1 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 18:52:18 +0100 Subject: [PATCH 15/42] Add option for admins to make deployments and VMs always active --- bun.lockb | Bin 253008 -> 253008 bytes package.json | 2 +- src/components/render/Resource.tsx | 7 ++- src/locales/en.json | 5 +- src/locales/se.json | 5 +- src/pages/edit/Edit.tsx | 10 +++- src/pages/edit/NeverStaleMode.tsx | 90 +++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 src/pages/edit/NeverStaleMode.tsx diff --git a/bun.lockb b/bun.lockb index 217371b7588d8885231810c763bccf11c0d60831..ecabe3ddd2abd4522291cd6a7127a797d62ab64a 100755 GIT binary patch delta 152 zcmV;J0B8TuxDU{{50EY(IfNL_oxa>*(Xn~^X$v&%Yt>i+`kRcWq9QZ&j`N+Tu}&&* zlXN&JlOS0Mv#D^5-9Qk*nW4S-jLN)2*a*-SB5JR@kk_Q0mGDP^^Z-)lt$=nDcHpdW zs$gRf!$JyeejaWyg)m?Z>51XZ6Wg^mq1=acP64-dP6GQs0W-I8jslD1mmx0$Ah#TI G12k#r(MX{H delta 154 zcmV;L0A>HsxDU{{50EY(S5--JZ>vXSamE*+x&(Z!_s<)%P~>WdNv)HR&DHdRu}&&* z0Th#ACnu92SqZbLaE#qRGodzyY4K$!D<6e_Xh=2>8_W!4B*e*anWh9idCF>;$Vy~j z1fj-|2={j diff --git a/package.json b/package.json index 272a3471..edc30538 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@iconify/react": "^4.1.1", - "@kthcloud/go-deploy-types": "^1.0.20", + "@kthcloud/go-deploy-types": "^1.0.23", "@mui/icons-material": "^5.15.20", "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.20", diff --git a/src/components/render/Resource.tsx b/src/components/render/Resource.tsx index c400ac17..fd582e5b 100644 --- a/src/components/render/Resource.tsx +++ b/src/components/render/Resource.tsx @@ -303,9 +303,10 @@ export const renderStale = ( : isOlderThanThreeMonths(row?.accessedAt); if ( - !stale && - (daysLeftUntilStale === false || - (daysLeftUntilStale as number) > warningDaysBeforeStale) + row.neverStale || + (!stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale)) ) return <>; diff --git a/src/locales/en.json b/src/locales/en.json index aae77e04..ea4b76db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -546,6 +546,9 @@ "stale-soon": "Stale soon", "stale-soon-description": "This resource will become stale soon due to inactivity from the owner", "stale-in": "Stale in", - "days-left": "days left" + "days-left": "days left", + "never-stale-option-header": "Always Active Option", + "never-stale-option-header-subheader": "Control whether this resource should remain active despite inactivity.", + "never-stale": "Always Active" } } diff --git a/src/locales/se.json b/src/locales/se.json index 5865aec3..a307c25f 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -545,6 +545,9 @@ "stale-soon": "Snart inaktiv", "stale-soon-description": "Denna resurs kommer snart att bli avaktiverad på grund av ägarens inaktivitet", "stale-in": "Inaktiv om", - "days-left": "dagar kvar" + "days-left": "dagar kvar", + "never-stale-option-header": "Aldrig inaktiv-alternativ", + "never-stale-option-header-subheader": "Styr om denna resurs ska förbli aktiv trots inaktivitet.", + "never-stale": "Alltid aktiv" } } diff --git a/src/pages/edit/Edit.tsx b/src/pages/edit/Edit.tsx index 556f509a..ad824890 100644 --- a/src/pages/edit/Edit.tsx +++ b/src/pages/edit/Edit.tsx @@ -51,6 +51,7 @@ import ProxyManager from "./vms/ProxyManager"; import { isOlderThanThreeMonths } from "../../components/render/Resource"; import Label from "../../components/Label"; import { getDaysLeftUntilStale } from "../../utils/staleDates"; +import NeverStaleMode from "./NeverStaleMode"; export function Edit() { const { t } = useTranslation(); @@ -204,9 +205,10 @@ export function Edit() { : isOlderThanThreeMonths(resource?.accessedAt); if ( - !stale && - (daysLeftUntilStale === false || - (daysLeftUntilStale as number) > warningDaysBeforeStale) + resource.neverStale || + (!stale && + (daysLeftUntilStale === false || + (daysLeftUntilStale as number) > warningDaysBeforeStale)) ) return <>; @@ -449,6 +451,8 @@ export function Edit() { )} + {user?.admin && } + diff --git a/src/pages/edit/NeverStaleMode.tsx b/src/pages/edit/NeverStaleMode.tsx new file mode 100644 index 00000000..7af52466 --- /dev/null +++ b/src/pages/edit/NeverStaleMode.tsx @@ -0,0 +1,90 @@ +import { + Card, + CardContent, + CardHeader, + FormControlLabel, + Switch, +} from "@mui/material"; +import { useKeycloak } from "@react-keycloak/web"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Job, Resource } from "../../types"; +import { enqueueSnackbar } from "notistack"; +import { updateDeployment } from "../../api/deploy/deployments"; +import { updateVM } from "../../api/deploy/vms"; +import useResource from "../../hooks/useResource"; +import { errorHandler } from "../../utils/errorHandler"; + +export default function NeverStaleMode({ resource }: { resource: Resource }) { + const { t } = useTranslation(); + const { keycloak, initialized } = useKeycloak(); + const { queueJob, beginFastLoad } = useResource(); + + const [neverStale, setNeverStale] = useState( + resource?.neverStale || false + ); + const [isUpdating, setIsUpdating] = useState(false); + useEffect(() => { + if (!isUpdating) setNeverStale(resource?.neverStale || false); + }, [resource, isUpdating]); + + const handleNeverStaleChange = async (neverStaleV: boolean) => { + if (!(initialized && resource && keycloak.token)) { + enqueueSnackbar(t("error-updating"), { variant: "error" }); + return; + } + + try { + let result: Job | null = null; + if (resource.type === "deployment") { + result = await updateDeployment( + resource.id, + { neverStale: neverStaleV }, + keycloak.token + ); + } else if (resource.type === "vm") { + result = await updateVM(keycloak.token, resource.id, { + //@ts-ignore not added yet + neverStale: neverStaleV, + }); + } + + if (result) { + queueJob(result); + beginFastLoad(); + enqueueSnackbar(t("saving-never-stale"), { variant: "info" }); + setNeverStale(neverStaleV); + setIsUpdating(true); + } + } catch (error: any) { + errorHandler(error).forEach((e) => + enqueueSnackbar(t("error-updating") + ": " + e, { + variant: "error", + }) + ); + } finally { + setIsUpdating(false); + } + }; + + return ( + + + + handleNeverStaleChange(e.target.checked)} + inputProps={{ "aria-label": "controlled" }} + /> + } + label={t("never-stale")} + /> + + + ); +} From 4b6afc6b0b3f678c894561db8806a129bdfe7b64 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 23 Jan 2025 18:55:05 +0100 Subject: [PATCH 16/42] fix tsc --- src/pages/create/CreateVm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/create/CreateVm.tsx b/src/pages/create/CreateVm.tsx index 414e73dd..7a29bf1f 100644 --- a/src/pages/create/CreateVm.tsx +++ b/src/pages/create/CreateVm.tsx @@ -169,6 +169,7 @@ export default function CreateVm({ diskSize: diskSize, ram: ram, ports: [], + neverStale: false, }; // zone: selectedZone, From fec670d4c4379179ede8827fb9d521384ca3c751 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 29 Jan 2025 20:03:04 +0100 Subject: [PATCH 17/42] improve performance --- src/App.tsx | 49 ++--- src/api/deploy/users.ts | 29 ++- src/components/admin/AdminToolbar.tsx | 83 +++++++++ src/components/admin/ResourceTab.tsx | 31 ++-- src/contexts/AdminResourceContext.tsx | 237 ++++++++++++++++++++---- src/hooks/useFilterableResourceState.ts | 20 +- src/pages/admin/AdminV2.tsx | 121 +++++++----- src/pages/admin/index.ts | 2 +- src/types.ts | 26 +++ src/utils/paguinationOpts.ts | 31 ++++ 10 files changed, 516 insertions(+), 113 deletions(-) create mode 100644 src/components/admin/AdminToolbar.tsx create mode 100644 src/utils/paguinationOpts.ts diff --git a/src/App.tsx b/src/App.tsx index 21597977..2bc1a813 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,35 +14,38 @@ import { IconButton } from "@mui/material"; import Iconify from "./components/Iconify"; import { ThemeModeContextProvider } from "./contexts/ThemeModeContext"; import { AlertContextProvider } from "./contexts/AlertContext"; +import { AdminResourceContextProvider } from "./contexts/AdminResourceContext"; export default function App() { return ( - - ( - closeSnackbar(snack)} - color="inherit" - > - - - )} - dense - preventDuplicate - > - - - - - - - + + + ( + closeSnackbar(snack)} + color="inherit" + > + + + )} + dense + preventDuplicate + > + + + + + + + + diff --git a/src/api/deploy/users.ts b/src/api/deploy/users.ts index 1246aa09..fbd1f457 100644 --- a/src/api/deploy/users.ts +++ b/src/api/deploy/users.ts @@ -4,7 +4,8 @@ import { UserReadDiscovery, UserUpdate, } from "@kthcloud/go-deploy-types/types/v2/body"; -import { Jwt, Uuid } from "../../types"; +import { Jwt, UserQueryParams, Uuid } from "../../types"; +import { createQueryParams } from "../../utils/paguinationOpts"; export const getUser = async ( userId: string, @@ -153,3 +154,29 @@ export const discoverUserById = async ( } return await res.json(); }; + +export const getUsers = async ( + token: Uuid, + queryParams: UserQueryParams = { all: true, page: 1, pageSize: 10 } +): Promise => { + const res = await fetch( + import.meta.env.VITE_DEPLOY_API_URL + + "/users" + + createQueryParams(queryParams), + { + method: "GET", + headers: { + Authorization: "Bearer " + token, + }, + } + ); + + if (!res.ok) { + const body = await res.json(); + if (body) { + throw body; + } + throw res; + } + return await res.json(); +}; diff --git a/src/components/admin/AdminToolbar.tsx b/src/components/admin/AdminToolbar.tsx new file mode 100644 index 00000000..819a7167 --- /dev/null +++ b/src/components/admin/AdminToolbar.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import useAdmin from "../../hooks/useAdmin"; +import useInterval from "../../hooks/useInterval"; +import { useTranslation } from "react-i18next"; +import { + AppBar, + Box, + Button, + Stack, + Toolbar, + Typography, + useTheme, +} from "@mui/material"; + +export default function AdminToolbar() { + const { t } = useTranslation(); + const theme = useTheme(); + const { lastRefresh, refetch, loading, lastRefreshRtt } = useAdmin(); + const [timeDiffSinceLastRefresh, setTimeDiffSinceLastRefresh] = + useState(""); + useInterval(() => { + const now = new Date().getTime(); + const diff = now - lastRefresh; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + setTimeDiffSinceLastRefresh(hours + " " + t("time-hours-ago")); + return; + } + if (minutes > 0) { + setTimeDiffSinceLastRefresh(minutes + " " + t("time-minutes-ago")); + return; + } + + if (seconds > 0) { + setTimeDiffSinceLastRefresh(seconds + " " + t("time-seconds-ago")); + return; + } + + setTimeDiffSinceLastRefresh("0 " + t("time-seconds-ago")); + }, 1000); + + return ( + + + {t("admin-title")} + + + + + {loading ? ( + t("loading") + ) : ( + + RTT: + + {" " + lastRefreshRtt + " ms "} + + {t("admin-last-load")}: + + {" " + timeDiffSinceLastRefresh} + + + )} + + + + + ); +} diff --git a/src/components/admin/ResourceTab.tsx b/src/components/admin/ResourceTab.tsx index a7d92387..68f565fc 100644 --- a/src/components/admin/ResourceTab.tsx +++ b/src/components/admin/ResourceTab.tsx @@ -42,6 +42,10 @@ interface ResourceTabProps { setCategory?: Dispatch>; queryModifier?: QueryModifier[Category]; setQueryModifier?: Dispatch>; + page: number; + setPage: Dispatch>; + pageSize: number; + setPageSize: Dispatch>; } const ResourceTab = ({ @@ -57,6 +61,10 @@ const ResourceTab = ({ setCategory, queryModifier, setQueryModifier, + page, + setPage, + pageSize, + setPageSize, }: ResourceTabProps) => { const { t } = useTranslation(); const theme = useTheme(); @@ -84,22 +92,23 @@ const ResourceTab = ({ : actions; const [selectedItem, setSelectedItem] = useState(undefined); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const handleChangePage = (_: any, newPage: number) => { - setPage(newPage); + if (page !== newPage) setPage(newPage); }; const handleChangeRowsPerPage = (event: any) => { - setRowsPerPage(parseInt(event.target.value, 10)); + if (event.target.value === "all") { + setPageSize(-1); + } else { + setPageSize(parseInt(event.target.value, 10)); + } setPage(0); }; const currentData = filter ? filteredData : data; const paginatedData = currentData?.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage + page * pageSize, + page * pageSize + pageSize ); // annoying TS compile issue for some reason @@ -213,13 +222,13 @@ const ResourceTab = ({ {selectedItem && OnClickModal ? ( ( - {[...Array(5)].map((_, rowIndex) => ( + {[...Array(10)].map((_, rowIndex) => ( {[...Array(columns)].map((_, colIndex) => ( - + ))} diff --git a/src/contexts/AdminResourceContext.tsx b/src/contexts/AdminResourceContext.tsx index e7af0793..946ebf44 100644 --- a/src/contexts/AdminResourceContext.tsx +++ b/src/contexts/AdminResourceContext.tsx @@ -3,6 +3,7 @@ import { Dispatch, SetStateAction, useEffect, + useMemo, useState, } from "react"; import useInterval from "../hooks/useInterval"; @@ -15,9 +16,11 @@ import { UserRead, VmRead, } from "@kthcloud/go-deploy-types/types/v2/body"; -import useFilterableResourceState from "../hooks/useFilterableResourceState"; +import useFilterableResourceState, { + DEFAULT_PAGESIZE, +} from "../hooks/useFilterableResourceState"; import { useKeycloak } from "@react-keycloak/web"; -import { getAllUsers } from "../api/deploy/users"; +import { getUsers } from "../api/deploy/users"; import { errorHandler } from "../utils/errorHandler"; import { enqueueSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; @@ -35,37 +38,77 @@ type AdminResourceContextType = { setEnableFetching: Dispatch>; lastRefresh: number; lastRefreshRtt: number; - timeDiffSinceLastRefresh: string; loading: boolean; refetch: () => void; + // Users users: UserRead[] | undefined; usersFilter: string | undefined; setUsersFilter: Dispatch>; filteredUsers: UserRead[] | undefined; + usersPage: number; + setUsersPage: Dispatch>; + usersPageSize: number; + setUsersPageSize: Dispatch>; + + // Teams teams: TeamRead[] | undefined; teamsFilter: string | undefined; setTeamsFilter: Dispatch>; filteredTeams: TeamRead[] | undefined; + teamsPage: number; + setTeamsPage: Dispatch>; + teamsPageSize: number; + setTeamsPageSize: Dispatch>; + + // Deployments deployments: DeploymentRead[] | undefined; deploymentsFilter: string | undefined; setDeploymentsFilter: Dispatch>; filteredDeployments: DeploymentRead[] | undefined; + deploymentsPage: number; + setDeploymentsPage: Dispatch>; + deploymentsPageSize: number; + setDeploymentsPageSize: Dispatch>; + + // Vms vms: VmRead[] | undefined; vmsFilter: string | undefined; setVmsFilter: Dispatch>; filteredVms: VmRead[] | undefined; + vmsPage: number; + setVmsPage: Dispatch>; + vmsPageSize: number; + setVmsPageSize: Dispatch>; + + // GpuLeases gpuLeases: GpuLeaseRead[] | undefined; gpuLeasesFilter: string | undefined; setGpuLeasesFilter: Dispatch>; filteredGpuLeases: GpuLeaseRead[] | undefined; + gpuLeasesPage: number; + setGpuLeasesPage: Dispatch>; + gpuLeasesPageSize: number; + setGpuLeasesPageSize: Dispatch>; + + // GpuGroups gpuGroups: GpuGroupRead[] | undefined; gpuGroupsFilter: string | undefined; setGpuGroupsFilter: Dispatch>; filteredGpuGroups: GpuGroupRead[] | undefined; + gpuGroupsPage: number; + setGpuGroupsPage: Dispatch>; + gpuGroupsPageSize: number; + setGpuGroupsPageSize: Dispatch>; + + // Jobs jobs: JobRead[] | undefined; jobsFilter: string | undefined; setJobsFilter: Dispatch>; filteredJobs: JobRead[] | undefined; + jobsPage: number; + setJobsPage: Dispatch>; + jobsPageSize: number; + setJobsPageSize: Dispatch>; }; const initialState: AdminResourceContextType = { @@ -73,37 +116,77 @@ const initialState: AdminResourceContextType = { setEnableFetching: () => {}, lastRefresh: 0, lastRefreshRtt: 0, - timeDiffSinceLastRefresh: "", loading: false, refetch: () => {}, + // Users users: undefined, usersFilter: undefined, setUsersFilter: () => {}, filteredUsers: undefined, + usersPage: 0, + setUsersPage: () => {}, + usersPageSize: DEFAULT_PAGESIZE, + setUsersPageSize: () => {}, + + // Teams teams: undefined, teamsFilter: undefined, setTeamsFilter: () => {}, filteredTeams: undefined, + teamsPage: 0, + setTeamsPage: () => {}, + teamsPageSize: DEFAULT_PAGESIZE, + setTeamsPageSize: () => {}, + + // Deployments deployments: undefined, deploymentsFilter: undefined, setDeploymentsFilter: () => {}, filteredDeployments: undefined, + deploymentsPage: 0, + setDeploymentsPage: () => {}, + deploymentsPageSize: DEFAULT_PAGESIZE, + setDeploymentsPageSize: () => {}, + + // Vms vms: undefined, vmsFilter: undefined, setVmsFilter: () => {}, filteredVms: undefined, + vmsPage: 0, + setVmsPage: () => {}, + vmsPageSize: DEFAULT_PAGESIZE, + setVmsPageSize: () => {}, + + // GpuLeases gpuLeases: undefined, gpuLeasesFilter: undefined, setGpuLeasesFilter: () => {}, filteredGpuLeases: undefined, + gpuLeasesPage: 0, + setGpuLeasesPage: () => {}, + gpuLeasesPageSize: DEFAULT_PAGESIZE, + setGpuLeasesPageSize: () => {}, + + // GpuGroups gpuGroups: undefined, gpuGroupsFilter: undefined, setGpuGroupsFilter: () => {}, filteredGpuGroups: undefined, + gpuGroupsPage: 0, + setGpuGroupsPage: () => {}, + gpuGroupsPageSize: DEFAULT_PAGESIZE, + setGpuGroupsPageSize: () => {}, + + // Jobs jobs: undefined, jobsFilter: undefined, setJobsFilter: () => {}, filteredJobs: undefined, + jobsPage: 0, + setJobsPage: () => {}, + jobsPageSize: DEFAULT_PAGESIZE, + setJobsPageSize: () => {}, }; export const AdminResourceContext = createContext(initialState); @@ -119,6 +202,10 @@ export const AdminResourceContextProvider = ({ filter: usersFilter, setFilter: setUsersFilter, filteredItems: filteredUsers, + page: usersPage, + setPage: setUsersPage, + pageSize: usersPageSize, + setPageSize: setUsersPageSize, } = useFilterableResourceState(undefined); const { items: teams, @@ -126,6 +213,10 @@ export const AdminResourceContextProvider = ({ filter: teamsFilter, setFilter: setTeamsFilter, filteredItems: filteredTeams, + page: teamsPage, + setPage: setTeamsPage, + pageSize: teamsPageSize, + setPageSize: setTeamsPageSize, } = useFilterableResourceState(undefined); const { items: deployments, @@ -133,6 +224,10 @@ export const AdminResourceContextProvider = ({ filter: deploymentsFilter, setFilter: setDeploymentsFilter, filteredItems: filteredDeployments, + page: deploymentsPage, + setPage: setDeploymentsPage, + pageSize: deploymentsPageSize, + setPageSize: setDeploymentsPageSize, } = useFilterableResourceState(undefined); const { items: vms, @@ -140,6 +235,10 @@ export const AdminResourceContextProvider = ({ filter: vmsFilter, setFilter: setVmsFilter, filteredItems: filteredVms, + page: vmsPage, + setPage: setVmsPage, + pageSize: vmsPageSize, + setPageSize: setVmsPageSize, } = useFilterableResourceState(undefined); const { items: gpuLeases, @@ -147,6 +246,10 @@ export const AdminResourceContextProvider = ({ filter: gpuLeasesFilter, setFilter: setGpuLeasesFilter, filteredItems: filteredGpuLeases, + page: gpuLeasesPage, + setPage: setGpuLeasesPage, + pageSize: gpuLeasesPageSize, + setPageSize: setGpuLeasesPageSize, } = useFilterableResourceState(undefined); const { items: gpuGroups, @@ -154,6 +257,10 @@ export const AdminResourceContextProvider = ({ filter: gpuGroupsFilter, setFilter: setGpuGroupsFilter, filteredItems: filteredGpuGroups, + page: gpuGroupsPage, + setPage: setGpuGroupsPage, + pageSize: gpuGroupsPageSize, + setPageSize: setGpuGroupsPageSize, } = useFilterableResourceState(undefined); const { items: jobs, @@ -161,11 +268,14 @@ export const AdminResourceContextProvider = ({ filter: jobsFilter, setFilter: setJobsFilter, filteredItems: filteredJobs, + page: jobsPage, + setPage: setJobsPage, + pageSize: jobsPageSize, + setPageSize: setJobsPageSize, } = useFilterableResourceState(undefined); const [lastRefreshRtt, setLastRefreshRtt] = useState(0); const [lastRefresh, setLastRefresh] = useState(0); - const [timeDiffSinceLastRefresh, setTimeDiffSinceLastRefresh] = useState(""); const [loading, setLoading] = useState(false); const { t } = useTranslation(); @@ -183,41 +293,31 @@ export const AdminResourceContextProvider = ({ t, setLastRefresh, setLastRefreshRtt, + usersPage, + usersPageSize, setUsers, + teamsPage, + teamsPageSize, setTeams, + deploymentsPage, + deploymentsPageSize, setDeployments, + vmsPage, + vmsPageSize, setVms, + gpuLeasesPage, + gpuLeasesPageSize, setGpuLeases, + gpuGroupsPage, + gpuGroupsPageSize, setGpuGroups, + jobsPage, + jobsPageSize, setJobs ).finally(() => setLoading(false)); } }; - useInterval(() => { - const now = new Date().getTime(); - const diff = now - lastRefresh; - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - - if (hours > 0) { - setTimeDiffSinceLastRefresh(hours + " " + t("time-hours-ago")); - return; - } - if (minutes > 0) { - setTimeDiffSinceLastRefresh(minutes + " " + t("time-minutes-ago")); - return; - } - - if (seconds > 0) { - setTimeDiffSinceLastRefresh(seconds + " " + t("time-seconds-ago")); - return; - } - - setTimeDiffSinceLastRefresh("0 " + t("time-seconds-ago")); - }, 1000); - useEffect(() => { if ( initialized && @@ -250,7 +350,6 @@ export const AdminResourceContextProvider = ({ setEnableFetching, lastRefresh, lastRefreshRtt, - timeDiffSinceLastRefresh, loading, refetch: () => { if (initialized && keycloak.authenticated && user && user.admin) { @@ -260,34 +359,76 @@ export const AdminResourceContextProvider = ({ } } }, + + // Users users, usersFilter, setUsersFilter, filteredUsers, + usersPage, + setUsersPage, + usersPageSize, + setUsersPageSize, + + // Teams teams, teamsFilter, setTeamsFilter, filteredTeams, + teamsPage, + setTeamsPage, + teamsPageSize, + setTeamsPageSize, + + // Deployments deployments, deploymentsFilter, setDeploymentsFilter, filteredDeployments, + deploymentsPage, + setDeploymentsPage, + deploymentsPageSize, + setDeploymentsPageSize, + + // Vms vms, vmsFilter, setVmsFilter, filteredVms, + vmsPage, + setVmsPage, + vmsPageSize, + setVmsPageSize, + + // GpuLeases gpuLeases, gpuLeasesFilter, setGpuLeasesFilter, filteredGpuLeases, + gpuLeasesPage, + setGpuLeasesPage, + gpuLeasesPageSize, + setGpuLeasesPageSize, + + // GpuGroups gpuGroups, gpuGroupsFilter, setGpuGroupsFilter, filteredGpuGroups, + gpuGroupsPage, + setGpuGroupsPage, + gpuGroupsPageSize, + setGpuGroupsPageSize, + + // Jobs jobs, jobsFilter, setJobsFilter, filteredJobs, + jobsPage, + setJobsPage, + jobsPageSize, + setJobsPageSize, }} > {children} @@ -301,21 +442,51 @@ async function fetchResources( t: TFunction<"translation", undefined>, setLastRefresh: Dispatch>, setLastRefreshRtt: Dispatch>, + + // Users + __usersPage: number, + __usersPageSize: number, setUsers: Dispatch>, + + // Teams + __teamsPage: number, + __teamsPageSize: number, setTeams: Dispatch>, + + // Deployments + __deploymentsPage: number, + __deploymentsPageSize: number, setDeployments: Dispatch>, + + // Vms + __vmsPage: number, + __vmsPageSize: number, setVms: Dispatch>, + + // GpuLeases + __gpuLeases: number, + __gpuLeasesPage: number, setGpuLeases: Dispatch>, + + // GpuGroups + __gpuGroups: number, + __gpuGroupsPageSize: number, setGpuGroups: Dispatch>, + + // Jobs + __jobsPage: number, + __jobsPageSize: number, setJobs: Dispatch> ) { if (!(initialized && keycloak.authenticated && keycloak.token)) return; - const startTimer = Date.now(); + const startTimer = performance.now(); const promises = [ async () => { try { - const response = await getAllUsers(keycloak.token!); + const response = await getUsers(keycloak.token!, { + all: true, + }); setUsers(response); } catch (error: any) { errorHandler(error).forEach((e) => @@ -410,5 +581,5 @@ async function fetchResources( // end timer and set last refresh, show in ms setLastRefresh(new Date().getTime()); - setLastRefreshRtt(Date.now() - startTimer); + setLastRefreshRtt(performance.now() - startTimer); } diff --git a/src/hooks/useFilterableResourceState.ts b/src/hooks/useFilterableResourceState.ts index 733ebf55..4e9e6ffb 100644 --- a/src/hooks/useFilterableResourceState.ts +++ b/src/hooks/useFilterableResourceState.ts @@ -1,4 +1,6 @@ -import { useState, useMemo, Dispatch, SetStateAction } from "react"; +import { useState, useMemo, Dispatch, SetStateAction, useEffect } from "react"; + +export const DEFAULT_PAGESIZE = 10; export default function useFilterableResourceState( defaultState: T[] | undefined @@ -8,9 +10,15 @@ export default function useFilterableResourceState( filter: string | undefined; setFilter: Dispatch>; filteredItems: T[] | undefined; + page: number; + setPage: Dispatch>; + pageSize: number; + setPageSize: Dispatch>; } { const [items, setItems] = useState(defaultState); const [filter, setFilter] = useState(undefined); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGESIZE); const filteredItems = useMemo(() => { if (filter === undefined) return undefined; @@ -19,11 +27,21 @@ export default function useFilterableResourceState( ); }, [items, filter]); + useEffect(() => { + if (page !== 0) { + setPage(0); + } + }, [filter]); + return { items, setItems, filter, setFilter, filteredItems, + page, + setPage, + pageSize, + setPageSize, }; } diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index c5ec48fe..e84aa13f 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -18,7 +18,7 @@ import { import { useTranslation } from "react-i18next"; import useResource from "../../hooks/useResource"; import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { enqueueSnackbar } from "notistack"; import useAdmin from "../../hooks/useAdmin"; import LoadingPage from "../../components/LoadingPage"; @@ -41,47 +41,85 @@ import { import { Resource, Uuid } from "../../types"; import TimeAgo from "../../components/admin/TimeAgo"; import { Category, QueryModifier } from "../../components/admin/searchTypes"; +import AdminToolbar from "../../components/admin/AdminToolbar"; export default function AdminV2() { const { t } = useTranslation(); - const theme = useTheme(); const { user, setImpersonatingDeployment, setImpersonatingVm } = useResource(); const { fetchingEnabled, setEnableFetching, - lastRefreshRtt, - timeDiffSinceLastRefresh, - loading, - refetch, + + // Users users, usersFilter, setUsersFilter, filteredUsers, + usersPage, + setUsersPage, + usersPageSize, + setUsersPageSize, + + // Teams teams, teamsFilter, setTeamsFilter, filteredTeams, + teamsPage, + setTeamsPage, + teamsPageSize, + setTeamsPageSize, + + // Deployments deployments, deploymentsFilter, setDeploymentsFilter, filteredDeployments, + deploymentsPage, + setDeploymentsPage, + deploymentsPageSize, + setDeploymentsPageSize, + + // Vms vms, vmsFilter, setVmsFilter, filteredVms, + vmsPage, + setVmsPage, + vmsPageSize, + setVmsPageSize, + + // GpuLeases gpuLeases, gpuLeasesFilter, setGpuLeasesFilter, filteredGpuLeases, + gpuLeasesPage, + setGpuLeasesPage, + gpuLeasesPageSize, + setGpuLeasesPageSize, + + // GpuGroups gpuGroups, gpuGroupsFilter, setGpuGroupsFilter, filteredGpuGroups, + gpuGroupsPage, + setGpuGroupsPage, + gpuGroupsPageSize, + setGpuGroupsPageSize, + + // Jobs jobs, jobsFilter, setJobsFilter, filteredJobs, + jobsPage, + setJobsPage, + jobsPageSize, + setJobsPageSize, } = useAdmin(); const navigate = useNavigate(); @@ -374,42 +412,70 @@ export default function AdminV2() { filter: deploymentsFilter, setFilter: setDeploymentsFilter, filteredData: filteredDeployments, + page: deploymentsPage, + setPage: setDeploymentsPage, + pageSize: deploymentsPageSize, + setPageSize: setDeploymentsPageSize, }, { data: vms, filter: vmsFilter, setFilter: setVmsFilter, filteredData: filteredVms, + page: vmsPage, + setPage: setVmsPage, + pageSize: vmsPageSize, + setPageSize: setVmsPageSize, }, { data: gpuLeases, filter: gpuLeasesFilter, setFilter: setGpuLeasesFilter, filteredData: filteredGpuLeases, + page: gpuLeasesPage, + setPage: setGpuLeasesPage, + pageSize: gpuLeasesPageSize, + setPageSize: setGpuLeasesPageSize, }, { data: gpuGroups, filter: gpuGroupsFilter, setFilter: setGpuGroupsFilter, filteredData: filteredGpuGroups, + page: gpuGroupsPage, + setPage: setGpuGroupsPage, + pageSize: gpuGroupsPageSize, + setPageSize: setGpuGroupsPageSize, }, { data: users, filter: usersFilter, setFilter: setUsersFilter, filteredData: filteredUsers, + page: usersPage, + setPage: setUsersPage, + pageSize: usersPageSize, + setPageSize: setUsersPageSize, }, { data: teams, filter: teamsFilter, setFilter: setTeamsFilter, filteredData: filteredTeams, + page: teamsPage, + setPage: setTeamsPage, + pageSize: teamsPageSize, + setPageSize: setTeamsPageSize, }, { data: jobs, filter: jobsFilter, setFilter: setJobsFilter, filteredData: filteredJobs, + page: jobsPage, + setPage: setJobsPage, + pageSize: jobsPageSize, + setPageSize: setJobsPageSize, }, ]; @@ -435,7 +501,7 @@ export default function AdminV2() { }, []); const tabs = resourceConfig.map((config, index) => ( - // Todo + key={index} resourceName={config.label} data={resourceLookup[index].data} @@ -448,6 +514,10 @@ export default function AdminV2() { setCategory={setCategoryTemp} queryModifier={queryModifierTemp} setQueryModifier={setQueryModifierTemp} + page={resourceLookup[index].page} + setPage={resourceLookup[index].setPage} + pageSize={resourceLookup[index].pageSize} + setPageSize={resourceLookup[index].setPageSize} /> )); @@ -457,42 +527,7 @@ export default function AdminV2() { ) : ( - - - {t("admin-title")} - - - - - {loading ? ( - t("loading") - ) : ( - - RTT: - - {" " + lastRefreshRtt + " ms "} - - {t("admin-last-load")}: - - {" " + timeDiffSinceLastRefresh} - - - )} - - - - + diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 1130c83d..bf3650ee 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -1 +1 @@ -export { Admin as default } from "./Admin"; +export { default as default } from "./AdminV2"; diff --git a/src/types.ts b/src/types.ts index aa23ae58..3d952614 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,3 +43,29 @@ export type DeployApiError = { validationErrors?: ValidationError; errors?: ErrorElement[]; }; + +export type PaginationOpts = { + query?: string; + page?: number; + pageSize?: number; +}; + +export type BaseQueryParams = PaginationOpts & { + all?: boolean; +}; + +export type UserQueryParams = BaseQueryParams & { + userId?: string; +}; + +export type DeploymentQueryParams = UserQueryParams & { + shared?: boolean; +}; + +export type VmQueryParams = UserQueryParams & { + shared?: boolean; +}; + +export type GpuLeaseQueryParams = BaseQueryParams & { + vmId?: string; +}; diff --git a/src/utils/paguinationOpts.ts b/src/utils/paguinationOpts.ts new file mode 100644 index 00000000..2be6ac84 --- /dev/null +++ b/src/utils/paguinationOpts.ts @@ -0,0 +1,31 @@ +import { + BaseQueryParams, + DeploymentQueryParams, + GpuLeaseQueryParams, + UserQueryParams, + VmQueryParams, +} from "../types"; + +export function createQueryParams( + params: + | BaseQueryParams + | UserQueryParams + | DeploymentQueryParams + | VmQueryParams + | GpuLeaseQueryParams +): string { + const searchParams = new URLSearchParams(); + + if (params.all !== undefined) searchParams.append("all", String(params.all)); + if ("userId" in params && params.userId) + searchParams.append("userId", params.userId); + if ("shared" in params && params.shared !== undefined) + searchParams.append("shared", String(params.shared)); + if ("vmId" in params && params.vmId) searchParams.append("vmId", params.vmId); + if (params.page !== undefined) + searchParams.append("page", String(params.page)); + if (params.pageSize !== undefined) + searchParams.append("pageSize", String(params.pageSize)); + + return searchParams.toString() ? `?${searchParams.toString()}` : ""; +} From 445dbf7da7a5f6e270d582947bae718da153cb3c Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 29 Jan 2025 20:05:54 +0100 Subject: [PATCH 18/42] fix label for pagination all option --- src/components/admin/ResourceTab.tsx | 2 +- src/contexts/AdminResourceContext.tsx | 1 - src/pages/admin/AdminV2.tsx | 7 +------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/admin/ResourceTab.tsx b/src/components/admin/ResourceTab.tsx index 68f565fc..095edd90 100644 --- a/src/components/admin/ResourceTab.tsx +++ b/src/components/admin/ResourceTab.tsx @@ -228,7 +228,7 @@ const ResourceTab = ({ onRowsPerPageChange={handleChangeRowsPerPage} showFirstButton={true} showLastButton={true} - rowsPerPageOptions={[10, 25, 50, 100, "all"]} + rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: "all" }]} /> {selectedItem && OnClickModal ? ( Date: Wed, 29 Jan 2025 22:49:46 +0100 Subject: [PATCH 19/42] host admin page --- bun.lockb | Bin 253008 -> 253008 bytes package.json | 2 +- src/api/deploy/hosts.ts | 25 ++++ src/api/deploy/systemCapacities.ts | 29 ++++ src/components/admin/AdminToolbar.tsx | 2 +- src/components/admin/BlinkingLED.tsx | 31 ++++ src/components/admin/HostMachine.tsx | 198 ++++++++++++++++++++++++++ src/components/admin/HostsTab.tsx | 109 ++++++++++++++ src/components/admin/TimeLeft.tsx | 53 +++++++ src/contexts/AdminResourceContext.tsx | 69 ++++++++- src/locales/en.json | 4 +- src/locales/se.json | 4 +- src/pages/admin/AdminV2.tsx | 45 +++--- 13 files changed, 545 insertions(+), 26 deletions(-) create mode 100644 src/api/deploy/hosts.ts create mode 100644 src/api/deploy/systemCapacities.ts create mode 100644 src/components/admin/BlinkingLED.tsx create mode 100644 src/components/admin/HostMachine.tsx create mode 100644 src/components/admin/HostsTab.tsx create mode 100644 src/components/admin/TimeLeft.tsx diff --git a/bun.lockb b/bun.lockb index ecabe3ddd2abd4522291cd6a7127a797d62ab64a..7c3af1f4db2072bad023a2aaadee34e6ddd66c29 100755 GIT binary patch delta 783 zcmXZaZ%9*77{~FmvuRqS2q~7O(_CqmAQ^j6Q7h0F#)ycO?+e^4x49;wnJqGrw|Q5dE#4F5pi8$0mfJm+`sJ@>hnGt`qf)RWkhZ(ghm zEL*+jtA4lmvKw7xH5L2DEM0#`6VA5T;)2~%4W0H~o?zGIoS?Zh{^?^+#212#3ap zn__EgZRUXAZqWR8S(3KLV-RAU5F%L$K`GMV(hEjPG|QlN>zSH!%GTVs`Zb0*dO3&s z{r^OW%xIR&x?gp=q-IFhoaNGL%+HPyVka!vjvX)~16jz#nu+I_S50cLLXK=~V7n^_ z;Hv6$%e>4|%2ROQG)}@tb9&PiaH}=99H>9QF8g6b67e7#>x~a!2ztYPc!Lo7@eZ%? z3d?lz2Mbt41d|A32H!D-Z)a1zzI0viM|4!6xEltP#aAe`5ZO zj~GS-Va#Fyi(0!+ZZIT`P=CQF#xSj(U69&l{jgM`9NT!aegZO(rmp*?H71sSZBik> u+`IkcG<(}xt=Y?UOTM;1;m*p2>V0Vq=8}kK4=cS>hhw^$ z{GETjeJ_rVhX>=WWlx{ik3H};{>raeR*rvvQsa(uPd|1%Dm@+3oo708A-N*!@}APD zpS3dsaTD!({gWP7p-y!bih{&lWjba3iWiEag~_moYMVl}6l#^!Hpm5=Xio22-=HYw z(CswxccGdkR7)oxl7TXz>XKAjxyaRLrWn}*3#{0RZAis-?7*r)QP!Xsx+*+u`w19UrxSW0@NnePxbV+%>EYBR3ec0XNZzTadVo zCfq?Y?!t$AXu*B>(JF_lL`M2q!t?0iSiOiQKE|2{4`aW=Yjnv}rzpr;BwWHW{ve#k zOo+r9`7GY!13qFz^*cqRE_Q}^6k{02C)rso)Xn8=(|&|1B=Y7Yq`)k%yF{+hz;_Ol vy)Kb$(te$d_@R?wZefjBpvfxc)GW6+l%UI29d+W`ytM4ro7F8&eU9xvuZFfO diff --git a/package.json b/package.json index edc30538..88e84bb1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@iconify/react": "^4.1.1", - "@kthcloud/go-deploy-types": "^1.0.23", + "@kthcloud/go-deploy-types": "^1.0.24", "@mui/icons-material": "^5.15.20", "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.20", diff --git a/src/api/deploy/hosts.ts b/src/api/deploy/hosts.ts new file mode 100644 index 00000000..e6f83d0c --- /dev/null +++ b/src/api/deploy/hosts.ts @@ -0,0 +1,25 @@ +import { HostVerboseRead } from "@kthcloud/go-deploy-types/types/v2/body"; +import { Uuid } from "../../types"; + +export const getHostsVerbose = async ( + token: Uuid +): Promise => { + const res = await fetch( + import.meta.env.VITE_DEPLOY_API_URL + "/hosts/verbose", + { + method: "GET", + headers: { + Authorization: "Bearer " + token, + }, + } + ); + + if (!res.ok) { + const body = await res.json(); + if (body) { + throw body; + } + throw res; + } + return await res.json(); +}; diff --git a/src/api/deploy/systemCapacities.ts b/src/api/deploy/systemCapacities.ts new file mode 100644 index 00000000..e4bc36e0 --- /dev/null +++ b/src/api/deploy/systemCapacities.ts @@ -0,0 +1,29 @@ +import { SystemCapacities } from "@kthcloud/go-deploy-types/types/v2/body"; +import { Uuid } from "../../types"; + +export const getSystemCapacities = async ( + token: Uuid, + n?: number +): Promise => { + const res = await fetch( + import.meta.env.VITE_DEPLOY_API_URL + + "/systemCapacities" + + (n !== undefined ? "?n=" + n : ""), + { + method: "GET", + headers: { + Authorization: "Bearer " + token, + }, + } + ); + + if (!res.ok) { + const body = await res.json(); + if (body) { + throw body; + } + throw res; + } + const cap = await res.json(); + return cap.length > 0 ? cap[0]?.capacities ?? undefined : undefined; +}; diff --git a/src/components/admin/AdminToolbar.tsx b/src/components/admin/AdminToolbar.tsx index 819a7167..94d4341c 100644 --- a/src/components/admin/AdminToolbar.tsx +++ b/src/components/admin/AdminToolbar.tsx @@ -67,7 +67,7 @@ export default function AdminToolbar() { RTT: - {" " + lastRefreshRtt + " ms "} + {" " + lastRefreshRtt.toFixed(1) + " ms "} {t("admin-last-load")}: diff --git a/src/components/admin/BlinkingLED.tsx b/src/components/admin/BlinkingLED.tsx new file mode 100644 index 00000000..155e6d7c --- /dev/null +++ b/src/components/admin/BlinkingLED.tsx @@ -0,0 +1,31 @@ +import { Box, styled } from "@mui/material"; + +const BlinkingLED = styled(Box, { + shouldForwardProp: (prop) => prop !== "status", +})<{ status: boolean }>(({ theme, status }) => ({ + width: 12, + height: 12, + borderRadius: "50%", + backgroundColor: status + ? theme.palette.success.main + : theme.palette.error.main, + animation: status ? "blinkGreen 1s infinite" : "blinkRed 2s infinite", + boxShadow: `0 0 1rem ${ + status ? theme.palette.success.main : theme.palette.error.main + }`, + + "@keyframes blinkGreen": { + "0%": { opacity: 1 }, + "50%": { opacity: 0.3 }, + "70%": { opacity: 0.9 }, + "100%": { opacity: 1 }, + }, + "@keyframes blinkRed": { + "0%": { opacity: 1 }, + "50%": { opacity: 0.3 }, + "70%": { opacity: 0.9 }, + "100%": { opacity: 1 }, + }, +})); + +export default BlinkingLED; diff --git a/src/components/admin/HostMachine.tsx b/src/components/admin/HostMachine.tsx new file mode 100644 index 00000000..6e694ded --- /dev/null +++ b/src/components/admin/HostMachine.tsx @@ -0,0 +1,198 @@ +import { + HostCapacities, + HostVerboseRead, +} from "@kthcloud/go-deploy-types/types/v2/body"; +import { Box, Chip, Typography, useTheme } from "@mui/material"; +import BlinkingLED from "./BlinkingLED"; +import TimeLeft from "./TimeLeft"; +import TimeAgo from "./TimeAgo"; +import { useTranslation } from "react-i18next"; +import Iconify from "../Iconify"; + +export default function HostMachine({ + host, + specs, +}: { + host: HostVerboseRead; + specs?: HostCapacities; +}) { + const { t } = useTranslation(); + const theme = useTheme(); + + return ( + + {host.displayName} + + + + {host.schedulable ? t("schedulable") : t("unschedulable")} + + + {host.deactivatedUntil && + new Date(host.deactivatedUntil).getTime() - new Date().getTime() > + 0 && ( + + + + )} + + {specs && ( + + {specs.cpuCore && specs.cpuCore.total > 0 && ( + + } + label={ + + {t("landing-hero-cpu")} + + {specs.cpuCore.total} + + + } + /> + + )} + {specs.gpu && specs.gpu.total > 0 && ( + + } + label={ + + {t("resource-gpus")} + + {specs.gpu.total} + + + } + /> + + )} + {specs.ram && specs.ram.total > 0 && ( + + } + label={ + + {t("memory")} + + {specs.ram.total + " GB"} + + + } + /> + + )} + + )} + + + + + {t("last-seen")}: + + + + + + + + {t("registered-at")}: + + + + + + + + ); +} diff --git a/src/components/admin/HostsTab.tsx b/src/components/admin/HostsTab.tsx new file mode 100644 index 00000000..d90d3f3a --- /dev/null +++ b/src/components/admin/HostsTab.tsx @@ -0,0 +1,109 @@ +import { Box, Grid, Paper, Skeleton, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import useAdmin from "../../hooks/useAdmin"; +import HostMachine from "./HostMachine"; +import { + HostCapacities, + HostVerboseRead, + SystemCapacities, +} from "@kthcloud/go-deploy-types/types/v2/body"; + +const convertHostsToMap = (systemCapacities: SystemCapacities | undefined) => { + return ( + systemCapacities?.hosts?.reduce( + (map, host) => { + const key = `${host.name}_${host.zone}`; + map[key] = host; + return map; + }, + {} as Record + ) || {} + ); +}; + +export default function HostsTab() { + const { t } = useTranslation(); + const { hosts, systemCapacities } = useAdmin(); + + const hostsMap = systemCapacities ? convertHostsToMap(systemCapacities) : {}; + + const groupedByZone = hosts?.reduce( + (acc, host) => { + if (!acc[host.zone]) acc[host.zone] = { enabled: [], disabled: [] }; + host.enabled + ? acc[host.zone].enabled.push(host) + : acc[host.zone].disabled.push(host); + return acc; + }, + {} as Record< + string, + { enabled: HostVerboseRead[]; disabled: HostVerboseRead[] } + > + ); + + if (groupedByZone) { + Object.keys(groupedByZone).forEach((zone) => { + groupedByZone[zone].enabled.sort((a, b) => a.name.localeCompare(b.name)); + groupedByZone[zone].disabled.sort((a, b) => a.name.localeCompare(b.name)); + }); + } + + return ( + + {hosts === undefined ? ( + + {Array.from({ length: 9 }).map((_, index) => ( + + + + + + ))} + + ) : groupedByZone && Object.keys(groupedByZone).length > 0 ? ( + Object.entries(groupedByZone).map(([zone, { enabled, disabled }]) => ( + + + {t("zone")}: {zone} + + {enabled.length > 0 && ( + <> + + {t("enabled")} + + + {enabled.map((host) => ( + + + + ))} + + + )} + {disabled.length > 0 && ( + <> + + {t("disabled")} + + + {disabled.map((host) => ( + + + + ))} + + + )} + + )) + ) : ( + + {t("no-hosts-available")} + + )} + + ); +} diff --git a/src/components/admin/TimeLeft.tsx b/src/components/admin/TimeLeft.tsx new file mode 100644 index 00000000..23adfe87 --- /dev/null +++ b/src/components/admin/TimeLeft.tsx @@ -0,0 +1,53 @@ +import React, { useState, useEffect } from "react"; +import { Typography } from "@mui/material"; + +const TimeLeft: React.FC<{ targetDate: string | undefined }> = ({ + targetDate, +}) => { + const [timeLeft, setTimeLeft] = useState(""); + + const calculateTimeLeft = (targetDate: string) => { + const now = new Date().getTime(); + const targetTime = new Date(targetDate).getTime(); + const diff = targetTime - now; + + if (diff <= 0) return "Expired"; + + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days} day${days > 1 ? "s" : ""} left`; + } + if (hours > 0) { + return `${hours} hour${hours > 1 ? "s" : ""} left`; + } + if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? "s" : ""} left`; + } + if (seconds > 0) { + return `${seconds} second${seconds > 1 ? "s" : ""} left`; + } + return "Just now"; + }; + + useEffect(() => { + if (targetDate) { + setTimeLeft(calculateTimeLeft(targetDate)); + + const interval = setInterval(() => { + if (targetDate) { + setTimeLeft(calculateTimeLeft(targetDate)); + } + }, 1000); + + return () => clearInterval(interval); + } + }, [targetDate]); + + return {timeLeft}; +}; + +export default TimeLeft; diff --git a/src/contexts/AdminResourceContext.tsx b/src/contexts/AdminResourceContext.tsx index b9c44d4d..f84de46b 100644 --- a/src/contexts/AdminResourceContext.tsx +++ b/src/contexts/AdminResourceContext.tsx @@ -10,7 +10,9 @@ import { DeploymentRead, GpuGroupRead, GpuLeaseRead, + HostVerboseRead, JobRead, + SystemCapacities, TeamRead, UserRead, VmRead, @@ -31,6 +33,8 @@ import { getTeams } from "../api/deploy/teams"; import { getJobs } from "../api/deploy/jobs"; import useResource from "../hooks/useResource"; import { TFunction } from "i18next"; +import { getHostsVerbose } from "../api/deploy/hosts"; +import { getSystemCapacities } from "../api/deploy/systemCapacities"; type AdminResourceContextType = { fetchingEnabled: boolean; @@ -108,6 +112,12 @@ type AdminResourceContextType = { setJobsPage: Dispatch>; jobsPageSize: number; setJobsPageSize: Dispatch>; + + // Hosts + hosts: HostVerboseRead[] | undefined; + + // SystemCapacities + systemCapacities: SystemCapacities | undefined; }; const initialState: AdminResourceContextType = { @@ -186,6 +196,12 @@ const initialState: AdminResourceContextType = { setJobsPage: () => {}, jobsPageSize: DEFAULT_PAGESIZE, setJobsPageSize: () => {}, + + // Hosts + hosts: undefined, + + // SystemCapacities + systemCapacities: undefined, }; export const AdminResourceContext = createContext(initialState); @@ -273,6 +289,12 @@ export const AdminResourceContextProvider = ({ setPageSize: setJobsPageSize, } = useFilterableResourceState(undefined); + const [hosts, setHosts] = useState(undefined); + + const [systemCapacities, setSystemCapacities] = useState< + SystemCapacities | undefined + >(undefined); + const [lastRefreshRtt, setLastRefreshRtt] = useState(0); const [lastRefresh, setLastRefresh] = useState(0); const [loading, setLoading] = useState(false); @@ -312,7 +334,9 @@ export const AdminResourceContextProvider = ({ setGpuGroups, jobsPage, jobsPageSize, - setJobs + setJobs, + setHosts, + setSystemCapacities ).finally(() => setLoading(false)); } }; @@ -428,6 +452,12 @@ export const AdminResourceContextProvider = ({ setJobsPage, jobsPageSize, setJobsPageSize, + + // Hosts + hosts, + + // SystemCapacities + systemCapacities, }} > {children} @@ -475,7 +505,13 @@ async function fetchResources( // Jobs __jobsPage: number, __jobsPageSize: number, - setJobs: Dispatch> + setJobs: Dispatch>, + + // Hosts + setHosts: Dispatch>, + + // SystemCapacities + setSystemCapacities: Dispatch> ) { if (!(initialized && keycloak.authenticated && keycloak.token)) return; @@ -574,6 +610,35 @@ async function fetchResources( ); } }, + + async () => { + try { + const response = await getHostsVerbose(keycloak.token!); + setHosts(response); + } catch (error: any) { + errorHandler(error).forEach((e) => + enqueueSnackbar(t("error-could-not-fetch-hosts") + ": " + e, { + variant: "error", + }) + ); + } + }, + + async () => { + try { + const response = await getSystemCapacities(keycloak.token!); + if (response) setSystemCapacities(response); + } catch (error: any) { + errorHandler(error).forEach((e) => + enqueueSnackbar( + t("error-could-not-fetch-system-capacities") + ": " + e, + { + variant: "error", + } + ) + ); + } + }, ]; await Promise.all(promises.map((p) => p())); diff --git a/src/locales/en.json b/src/locales/en.json index ea4b76db..0f3febca 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -549,6 +549,8 @@ "days-left": "days left", "never-stale-option-header": "Always Active Option", "never-stale-option-header-subheader": "Control whether this resource should remain active despite inactivity.", - "never-stale": "Always Active" + "never-stale": "Always Active", + "error-could-not-fetch-hosts": "Could not fetch hosts", + "error-could-not-fetch-system-capacities": "Could not fetch system capacities" } } diff --git a/src/locales/se.json b/src/locales/se.json index a307c25f..6818604e 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -548,6 +548,8 @@ "days-left": "dagar kvar", "never-stale-option-header": "Aldrig inaktiv-alternativ", "never-stale-option-header-subheader": "Styr om denna resurs ska förbli aktiv trots inaktivitet.", - "never-stale": "Alltid aktiv" + "never-stale": "Alltid aktiv", + "error-could-not-fetch-hosts": "Kunde inte hämta värdmaskiner", + "error-could-not-fetch-system-capacities": "Kunde inte hämta kapaciteten hos systemet" } } diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index d3e76142..8d21f02d 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -37,6 +37,7 @@ import { Resource, Uuid } from "../../types"; import TimeAgo from "../../components/admin/TimeAgo"; import { Category, QueryModifier } from "../../components/admin/searchTypes"; import AdminToolbar from "../../components/admin/AdminToolbar"; +import HostsTab from "../../components/admin/HostsTab"; export default function AdminV2() { const { t } = useTranslation(); @@ -495,26 +496,29 @@ export default function AdminV2() { }; }, []); - const tabs = resourceConfig.map((config, index) => ( - - key={index} - resourceName={config.label} - data={resourceLookup[index].data} - filteredData={resourceLookup[index].filteredData} - filter={resourceLookup[index].filter} - setFilter={resourceLookup[index].setFilter} - columns={config.columns} - actions={config.actions} - category={categoryTemp} - setCategory={setCategoryTemp} - queryModifier={queryModifierTemp} - setQueryModifier={setQueryModifierTemp} - page={resourceLookup[index].page} - setPage={resourceLookup[index].setPage} - pageSize={resourceLookup[index].pageSize} - setPageSize={resourceLookup[index].setPageSize} - /> - )); + const tabs = [ + ...resourceConfig.map((config, index) => ( + + key={index} + resourceName={config.label} + data={resourceLookup[index].data} + filteredData={resourceLookup[index].filteredData} + filter={resourceLookup[index].filter} + setFilter={resourceLookup[index].setFilter} + columns={config.columns} + actions={config.actions} + category={categoryTemp} + setCategory={setCategoryTemp} + queryModifier={queryModifierTemp} + setQueryModifier={setQueryModifierTemp} + page={resourceLookup[index].page} + setPage={resourceLookup[index].setPage} + pageSize={resourceLookup[index].pageSize} + setPageSize={resourceLookup[index].setPageSize} + /> + )), + , + ]; return ( <> @@ -534,6 +538,7 @@ export default function AdminV2() { {resourceConfig.map((resource, index) => ( ))} + {tabs[activeTab]} From d62a1df237351a2a324da027e2350d76ab55ad7c Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 29 Jan 2025 22:51:58 +0100 Subject: [PATCH 20/42] fix tsc --- src/components/admin/HostMachine.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/admin/HostMachine.tsx b/src/components/admin/HostMachine.tsx index 6e694ded..1de626d8 100644 --- a/src/components/admin/HostMachine.tsx +++ b/src/components/admin/HostMachine.tsx @@ -21,6 +21,7 @@ export default function HostMachine({ return ( {host.displayName} From c394b88c58dc5f3fa36fdacd181a3549707a6c6d Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Wed, 29 Jan 2025 23:03:46 +0100 Subject: [PATCH 21/42] add conditional hostname coloring --- src/components/admin/HostMachine.tsx | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/admin/HostMachine.tsx b/src/components/admin/HostMachine.tsx index 1de626d8..b9e80128 100644 --- a/src/components/admin/HostMachine.tsx +++ b/src/components/admin/HostMachine.tsx @@ -19,6 +19,11 @@ export default function HostMachine({ const { t } = useTranslation(); const theme = useTheme(); + const currentlyDeactivated = + host.deactivatedUntil && + new Date(host.deactivatedUntil).getTime() - new Date().getTime() > 0; + + const hasIssue = !host.schedulable || currentlyDeactivated; return ( - {host.displayName} + + {host.displayName} + {host.schedulable ? t("schedulable") : t("unschedulable")} - {host.deactivatedUntil && - new Date(host.deactivatedUntil).getTime() - new Date().getTime() > - 0 && ( - - - - )} + {currentlyDeactivated && ( + + + + )} {specs && ( Date: Wed, 29 Jan 2025 23:15:18 +0100 Subject: [PATCH 22/42] make admin tabs more accesible on mobile --- src/pages/admin/AdminV2.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/admin/AdminV2.tsx b/src/pages/admin/AdminV2.tsx index 8d21f02d..39da279e 100644 --- a/src/pages/admin/AdminV2.tsx +++ b/src/pages/admin/AdminV2.tsx @@ -534,7 +534,11 @@ export default function AdminV2() { {t("menu-admin-panel")} - + {resourceConfig.map((resource, index) => ( ))} From 0b9dec083b33e4a9cdacd695213fa7f99c56c3a9 Mon Sep 17 00:00:00 2001 From: Philip Zingmark Date: Thu, 30 Jan 2025 00:24:12 +0100 Subject: [PATCH 23/42] /routes for tabs on admin page --- src/Router.tsx | 2 +- src/components/admin/ResourceTab.tsx | 22 ++++++++++++--------- src/pages/admin/AdminV2.tsx | 29 +++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index d87d9751..9abc0097 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -93,7 +93,7 @@ export default function Router() { ), }, { - path: "admin", + path: "admin/:tab?", element: ( diff --git a/src/components/admin/ResourceTab.tsx b/src/components/admin/ResourceTab.tsx index 095edd90..cf47eec6 100644 --- a/src/components/admin/ResourceTab.tsx +++ b/src/components/admin/ResourceTab.tsx @@ -83,10 +83,10 @@ const ResourceTab = ({ const resolvedActions = columns && columns.length > 0 ? [ - { + /*{ label: t("details"), onClick: (value: T) => setSelectedItem(value), - }, + },*/ ...(actions || []), ] : actions; @@ -149,8 +149,10 @@ const ResourceTab = ({ - {resolvedColumns.map((col) => ( - {col.label} + {resolvedColumns.map((col, index) => ( + + {col.label} + ))} {resolvedActions && ( @@ -161,9 +163,9 @@ const ResourceTab = ({ {paginatedData?.map((row, index) => ( - - {resolvedColumns.map((col) => ( - + + {resolvedColumns.map((col, j) => ( + {(() => { if (col.renderFunc) { if (col.id === "*") { @@ -189,10 +191,11 @@ const ResourceTab = ({ ))} {resolvedActions && ( - - {resolvedActions.map((action) => + + {resolvedActions.map((action, j) => action.withConfirm ? ( { @@ -204,6 +207,7 @@ const ResourceTab = ({ /> ) : (