From 9d40ae0e31ac6a36e31296b292380e1a476c6150 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 4 May 2026 14:23:50 +0200 Subject: [PATCH 01/19] Update packages Fix compilation error --- frontend-web/webclient/app/DefaultObjects.ts | 5 ++++- frontend-web/webclient/package.json | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index 99f4ef9c89..91c22390b2 100644 --- a/frontend-web/webclient/app/DefaultObjects.ts +++ b/frontend-web/webclient/app/DefaultObjects.ts @@ -10,10 +10,12 @@ import {ProviderBrandingResponse} from "./UCloud/ProviderBrandingApi"; import {initProviderBranding} from "./ProviderBrandings/AutomaticProviderBranding"; import {BrandingResponse} from "./UCloud/BrandingApi"; import {initBranding} from "./Applications/Branding/AutomaticBranding"; +import {SidebarTabId} from "./ui-components/SidebarComponents"; export interface StatusReduxObject { title: string; loading: boolean; + tab: SidebarTabId; } /** @@ -41,7 +43,8 @@ declare global { export function initStatus(): StatusReduxObject { return ({ title: "", - loading: false + loading: false, + tab: SidebarTabId.NONE, }); } diff --git a/frontend-web/webclient/package.json b/frontend-web/webclient/package.json index 99420429fc..8dcb38e31b 100644 --- a/frontend-web/webclient/package.json +++ b/frontend-web/webclient/package.json @@ -37,22 +37,22 @@ "date-fns": "4.1.0", "deepcopy": "2.1.0", "fast-deep-equal": "3.1.3", - "fuse.js": "7.1.0", + "fuse.js": "7.3.0", "immer": "10.2.0", - "jsrsasign": "11.1.1", + "jsrsasign": "11.1.3", "localforage": "1.10.0", "magic-bytes.js": "1.13.0", "mermaid": "11.14.0", "monaco-editor": "0.55.1", "monaco-vim": "0.4.2", - "react": "19.2.4", + "react": "19.2.5", "react-datepicker": "9.0.0", - "react-dom": "19.2.4", + "react-dom": "19.2.5", "react-markdown": "10.1.0", "react-modal": "3.16.3", "react-redux": "9.2.0", - "react-router": "7.13.0", - "react-router-dom": "7.13.0", + "react-router": "7.14.2", + "react-router-dom": "7.14.2", "react-syntax-highlighter": "16.1.0", "react-transition-group": "4.4.5", "react-virtualized-auto-sizer": "1.0.26", @@ -64,10 +64,10 @@ "ua-parser-js": "1.0.41", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", - "yaml": "2.8.3" + "yaml": "2.8.4" }, "devDependencies": { - "@playwright/test": "1.58.2", + "@playwright/test": "1.59.1", "@types/d3": "7.4.3", "@types/history": "5.0.0", "@types/json-schema": "7.0.15", From ff03691ce84f6822e2323e0e5f75b60bff43cc42 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 6 May 2026 13:23:03 +0200 Subject: [PATCH 02/19] Some typesafety fixes --- .../app/Accounting/Allocations/State.tsx | 54 +++++++++---------- .../webclient/app/Authentication/DataHook.ts | 6 +-- frontend-web/webclient/app/Core.tsx | 4 +- .../webclient/app/UCloud/ProvidersApi.tsx | 6 +-- .../app/Utilities/CollectionUtilities.tsx | 2 +- 5 files changed, 31 insertions(+), 41 deletions(-) diff --git a/frontend-web/webclient/app/Accounting/Allocations/State.tsx b/frontend-web/webclient/app/Accounting/Allocations/State.tsx index 95e15a7891..7c82a8a900 100644 --- a/frontend-web/webclient/app/Accounting/Allocations/State.tsx +++ b/frontend-web/webclient/app/Accounting/Allocations/State.tsx @@ -15,16 +15,10 @@ import { } from "@/Accounting"; import {ProjectInfo, projectInfoPi, projectInfoTitle} from "@/Project/InfoCache"; import {Client} from "@/Authentication/HttpClientInstance"; -import UsageCore2, {UsageReport, usageReportRetrieve} from "@/Accounting/UsageCore2"; +import {UsageReport, usageReportRetrieve} from "@/Accounting/UsageCore2"; import {timestampUnixMs} from "@/UtilityFunctions"; -import {useImmerState} from "@/Utilities/Immer"; import {produce} from "immer"; import {Feature} from "@/Features"; -import {SimpleRichItem} from "@/ui-components/RichSelect"; -import {largeModalStyle} from "@/Utilities/ModalUtilities"; -import {classConcat} from "@/Unstyled"; -import {CardClass} from "@/ui-components/Card"; -import {Flex, Icon, Input} from "@/ui-components"; import {getProviderTitle, getShortProviderTitle} from "@/Providers/ProviderTitle"; const fuzzyMatcher = newFuzzyMatchFuse<{title: string}, "title">(["title"]); @@ -86,25 +80,25 @@ export interface SubProjectFilter { // State reducer // ===================================================================================================================== export type UIAction = - | { type: "Reset" } - | { type: "WalletsLoaded", wallets: Accounting.WalletV2[]; } - | { type: "ManagedProvidersLoaded", providerIds: string[] } - | { type: "ManagedProductsLoaded", products: Record } - | { type: "GiftsLoaded", gifts: Gifts.GiftWithCriteria[] } - | { type: "UpdateSearchQuery", newQuery: string } - | { type: "SetEditing", recipientIdx: number, groupIdx: number, allocationIdx: number, isEditing: boolean } - | { type: "UpdateAllocation", allocationIdx: number, groupIdx: number, recipientIdx: number, newQuota: number, newStart: Date, newEnd: Date } - | { type: "UpdateGift", data: Partial } - | { type: "GiftCreated", gift: Gifts.GiftWithCriteria } - | { type: "GiftDeleted", id: number } - | { type: "UpdateRootAllocations", data: Partial } - | { type: "ResetRootAllocation" } - | { type: "ToggleViewOnlyProjects" } - | { type: "SortSubprojects", sortBy?: string, ascending: boolean } - | { type: "SubProjectData", projects: Record } - | { type: "UsageReportLoaded", reports: UsageReport[] } - | { type: "SubProjectFilterSettingUpdated", setting: SubProjectFilterSetting, newValue: string | undefined, enabled: boolean} - | { type: "SubProjectFilterSettingsLoad", settings: Record } + | {type: "Reset"} + | {type: "WalletsLoaded", wallets: Accounting.WalletV2[];} + | {type: "ManagedProvidersLoaded", providerIds: string[]} + | {type: "ManagedProductsLoaded", products: Record} + | {type: "GiftsLoaded", gifts: Gifts.GiftWithCriteria[]} + | {type: "UpdateSearchQuery", newQuery: string} + | {type: "SetEditing", recipientIdx: number, groupIdx: number, allocationIdx: number, isEditing: boolean} + | {type: "UpdateAllocation", allocationIdx: number, groupIdx: number, recipientIdx: number, newQuota: number, newStart: Date, newEnd: Date} + | {type: "UpdateGift", data: Partial} + | {type: "GiftCreated", gift: Gifts.GiftWithCriteria} + | {type: "GiftDeleted", id: number} + | {type: "UpdateRootAllocations", data: Partial} + | {type: "ResetRootAllocation"} + | {type: "ToggleViewOnlyProjects"} + | {type: "SortSubprojects", sortBy?: string, ascending: boolean} + | {type: "SubProjectData", projects: Record} + | {type: "UsageReportLoaded", reports: UsageReport[]} + | {type: "SubProjectFilterSettingUpdated", setting: SubProjectFilterSetting, newValue: string | undefined, enabled: boolean} + | {type: "SubProjectFilterSettingsLoad", settings: Record} ; export enum SubProjectFilterSetting { @@ -497,7 +491,7 @@ export function stateReducer(state: State, action: UIAction): State { } case "UsageReportLoaded": { - const newState= produce(state, draft => { + const newState = produce(state, draft => { draft.remoteData.reports = action.reports; }); @@ -559,7 +553,7 @@ export function stateReducer(state: State, action: UIAction): State { } if (result.length === 0) { - result.push({ key: "nooptions", title: "No options available" }); + result.push({key: "nooptions", title: "No options available"}); } return result.sort((a, b) => a.title.localeCompare(b.title)); @@ -728,7 +722,7 @@ export function stateReducer(state: State, action: UIAction): State { if (!state.subprojectSortByAscending) { return naturalOrderResult * -1; - } else { + } else { return naturalOrderResult; } }); @@ -761,7 +755,7 @@ export function stateReducer(state: State, action: UIAction): State { // ===================================================================================================================== export type UIEvent = UIAction - | { type: "Init" } + | {type: "Init"} ; export function useEventReducer(didCancel: React.RefObject, doDispatch: (action: UIAction) => void): (event: UIEvent) => unknown { diff --git a/frontend-web/webclient/app/Authentication/DataHook.ts b/frontend-web/webclient/app/Authentication/DataHook.ts index a1bdb75eac..0974a04365 100644 --- a/frontend-web/webclient/app/Authentication/DataHook.ts +++ b/frontend-web/webclient/app/Authentication/DataHook.ts @@ -183,7 +183,7 @@ export async function callAPIWithErrorHandler( ): Promise { try { return await callAPI(parameters); - } catch (e) { + } catch (e: any) { defaultErrorHandler(e); return null; } @@ -263,7 +263,7 @@ export function useAsyncWork(): AsyncWorker { setIsLoading(true); try { await fn(); - } catch (e) { + } catch (e: any) { if (didCancel) return; if (e.request) { const why = e.response?.why ?? e.request.statusText; @@ -318,7 +318,7 @@ export function useCloudAPI( if (!didCancel) { dispatch({type: "FETCH_SUCCESS", payload: result}); } - } catch (e) { + } catch (e: any) { if (!didCancel) { const statusCode = e.request.status; const why = e.response?.why ?? "An error occurred. Please reload the page."; diff --git a/frontend-web/webclient/app/Core.tsx b/frontend-web/webclient/app/Core.tsx index 81b12d32f7..b8ca9c6b41 100755 --- a/frontend-web/webclient/app/Core.tsx +++ b/frontend-web/webclient/app/Core.tsx @@ -263,8 +263,8 @@ interface RequireAuthOpts { requireSla?: boolean; } -function requireAuth(Delegate: React.FunctionComponent, opts?: RequireAuthOpts): React.FunctionComponent { - return function Auth(props: React.PropsWithChildren) { +function requireAuth(Delegate: React.FunctionComponent, opts?: RequireAuthOpts): (props: React.PropsWithChildren) => React.ReactNode { + return function Auth(props: React.PropsWithChildren): React.ReactNode { const info = Client.userInfo; if (!Client.isLoggedIn || info === undefined) { diff --git a/frontend-web/webclient/app/UCloud/ProvidersApi.tsx b/frontend-web/webclient/app/UCloud/ProvidersApi.tsx index 277d2e0671..3080ab1fe8 100644 --- a/frontend-web/webclient/app/UCloud/ProvidersApi.tsx +++ b/frontend-web/webclient/app/UCloud/ProvidersApi.tsx @@ -58,10 +58,6 @@ export interface Provider extends Resource void; } @@ -92,7 +88,7 @@ class ProviderApi extends ResourceApi & ProviderCallbacks>[] { + public retrieveOperations(): Operation>[] { return [ { text: "Create " + this.title.toLowerCase(), diff --git a/frontend-web/webclient/app/Utilities/CollectionUtilities.tsx b/frontend-web/webclient/app/Utilities/CollectionUtilities.tsx index a230e0ee95..8729b9708d 100644 --- a/frontend-web/webclient/app/Utilities/CollectionUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/CollectionUtilities.tsx @@ -61,7 +61,7 @@ export function fuzzyMatch(item: T, keys: K[], query: stri } export function newFuzzyMatchFuse(keys: K[]): Fuse { - return new Fuse( + return new Fuse( [], { threshold: 0.1, From 52d58797d9255748e913bbbb11b654cc77d1f3cf Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 13 May 2026 13:16:15 +0200 Subject: [PATCH 03/19] Re-write every Reducer to use `createSlice` instead. Typesafety work - including extending generics for ResourceBrowser-things Make catch-errors type 'any' explicitly Simplify flat-mapping (Widgets/Machines) Have markdown elements as closures instead of just passing the function Update tsconfig --- .../Accounting/Allocations/CommonSections.tsx | 142 +++--- .../Allocations/ProviderOnlySections.tsx | 8 +- .../webclient/app/Admin/Providers/Browse.tsx | 3 +- .../webclient/app/Admin/SupportPage.tsx | 2 +- .../webclient/app/Admin/UserCreation.tsx | 8 +- .../Branding/AutomaticBranding.tsx | 84 ++-- .../app/Applications/FavoriteToggle.tsx | 8 +- .../Jobs/CatalogDiscoveryMode.tsx | 2 +- .../app/Applications/Jobs/Create.tsx | 2 +- .../app/Applications/Jobs/JobViz/Renderer.tsx | 36 +- .../Jobs/JobViz/StreamProcessor.ts | 10 +- .../app/Applications/Jobs/JobsBrowse.tsx | 5 +- .../Applications/Jobs/Resources/Folders.tsx | 3 +- .../Applications/Jobs/Resources/Ingress.tsx | 3 +- .../Jobs/Resources/NetworkIPs.tsx | 2 +- .../app/Applications/Jobs/Resources/Peers.tsx | 3 +- .../Jobs/Resources/PrivateNetworks.tsx | 130 +++--- .../app/Applications/Jobs/Resources/index.tsx | 4 +- .../webclient/app/Applications/Jobs/View.tsx | 2 +- .../Applications/Jobs/Widgets/Machines.tsx | 12 +- .../Applications/Jobs/Widgets/ModuleList.tsx | 18 +- .../app/Applications/LicenseBrowse.tsx | 6 +- .../NetworkIP/NetworkIPBrowse.tsx | 8 +- .../PrivateNetwork/PrivateNetworkBrowse.tsx | 20 +- .../PublicLinks/PublicLinkBrowse.tsx | 8 +- .../app/Applications/Redux/Actions.ts | 36 -- .../app/Applications/Redux/Reducer.ts | 45 +- .../app/Applications/SshKeys/Add.tsx | 2 +- .../app/Applications/Studio/Applications.tsx | 2 +- .../app/Applications/Studio/Categories.tsx | 5 +- .../app/Applications/Studio/Group.tsx | 3 +- .../app/Applications/Studio/Groups.tsx | 2 +- .../Applications/Studio/SpotlightsEditor.tsx | 4 +- .../webclient/app/Authentication/lib.ts | 8 +- .../webclient/app/Dashboard/Dashboard.tsx | 202 +++++---- .../webclient/app/Dashboard/Redux/index.tsx | 47 +- frontend-web/webclient/app/DefaultObjects.ts | 4 +- frontend-web/webclient/app/Editor/Editor.tsx | 10 +- .../app/ErrorBoundary/ErrorBoundary.tsx | 2 +- .../webclient/app/Files/DriveBrowse.tsx | 8 +- .../webclient/app/Files/FileBrowse.tsx | 10 +- frontend-web/webclient/app/Files/FileTree.tsx | 5 +- .../webclient/app/Files/HTML5FileSelector.ts | 20 +- frontend-web/webclient/app/Files/Shares.tsx | 5 +- .../webclient/app/Files/SharesOutgoing.tsx | 25 +- frontend-web/webclient/app/Files/Uploader.tsx | 8 +- frontend-web/webclient/app/Grants/Editor.tsx | 8 +- frontend-web/webclient/app/Login/Login.tsx | 20 +- .../webclient/app/Navigation/Redux/index.tsx | 75 +--- .../webclient/app/Project/ProjectSwitcher.tsx | 2 +- .../webclient/app/Project/ReduxState.ts | 31 +- .../AutomaticProviderBranding.tsx | 31 +- .../webclient/app/Providers/Connection.tsx | 2 +- .../app/Resource/PermissionEditor.tsx | 12 +- .../webclient/app/Resource/Properties.tsx | 14 +- .../webclient/app/Resource/Router.tsx | 9 +- .../app/ServiceLicenseAgreement/index.tsx | 4 +- .../webclient/app/Stacks/StackView.tsx | 414 +++++++++--------- .../webclient/app/Stacks/StacksBrowse.tsx | 3 +- .../webclient/app/Terminal/Container.tsx | 24 +- frontend-web/webclient/app/Terminal/State.ts | 69 ++- frontend-web/webclient/app/Types/index.ts | 2 - frontend-web/webclient/app/UCX/UcxView.tsx | 22 +- .../app/UCloud/FileCollectionsApi.tsx | 2 +- .../webclient/app/UCloud/FilesApi.tsx | 25 +- frontend-web/webclient/app/UCloud/JobsApi.tsx | 4 +- .../webclient/app/UCloud/LicenseApi.tsx | 2 +- frontend-web/webclient/app/UCloud/Messages.ts | 6 +- .../app/UCloud/MetadataNamespaceApi.tsx | 2 +- .../webclient/app/UCloud/NetworkIPApi.tsx | 2 +- .../app/UCloud/PrivateNetworkApi.tsx | 2 +- .../webclient/app/UCloud/ProvidersApi.tsx | 15 +- .../webclient/app/UCloud/ResourceApi.tsx | 11 +- .../webclient/app/UCloud/SharesApi.tsx | 8 +- .../webclient/app/UserSettings/Avataaar.tsx | 4 +- .../webclient/app/UserSettings/Redux.ts | 59 +-- .../app/UserSettings/TwoFactorSetup.tsx | 8 +- .../app/UserSettings/UserSettings.tsx | 12 +- .../webclient/app/Utilities/ReduxHooks.ts | 73 ++- .../app/Utilities/ReduxUtilities.tsx | 25 +- .../webclient/app/ui-components/Card.tsx | 5 +- .../app/ui-components/ClickableDropdown.tsx | 8 +- .../app/ui-components/ConfirmationAction.tsx | 2 +- .../webclient/app/ui-components/Icon.tsx | 17 +- .../webclient/app/ui-components/Markdown.tsx | 10 +- .../webclient/app/ui-components/Operation.tsx | 18 +- .../webclient/app/ui-components/PopIn.tsx | 37 +- .../app/ui-components/ResourceBrowser.tsx | 12 +- .../app/ui-components/ScaffoldedForm.tsx | 8 +- .../webclient/app/ui-components/Sidebar.tsx | 248 ++++++----- .../app/ui-components/TemporalLineChart.tsx | 21 +- .../app/ui-components/ThemeToggle.tsx | 6 +- frontend-web/webclient/package-lock.json | 97 ++-- frontend-web/webclient/package.json | 2 +- frontend-web/webclient/tsconfig.json | 12 +- 95 files changed, 1216 insertions(+), 1271 deletions(-) delete mode 100644 frontend-web/webclient/app/Applications/Redux/Actions.ts diff --git a/frontend-web/webclient/app/Accounting/Allocations/CommonSections.tsx b/frontend-web/webclient/app/Accounting/Allocations/CommonSections.tsx index 7e874c3409..62c50d28d9 100644 --- a/frontend-web/webclient/app/Accounting/Allocations/CommonSections.tsx +++ b/frontend-web/webclient/app/Accounting/Allocations/CommonSections.tsx @@ -163,8 +163,8 @@ export const YourAllocations: React.FunctionComponent<{ } right={ {tree.usageAndQuota.map((uq, idx) => - - + + )} } indent={indent} @@ -177,7 +177,7 @@ export const YourAllocations: React.FunctionComponent<{ {wallet.category.name} } right={ - + } indent={indent * 2} > @@ -729,7 +729,7 @@ export const KeyMetrics: React.FunctionComponent<{ const atRiskPercentage = total > 0 ? (atRisk / total) * 100 : 0; const underusedPercentage = underused > 0 ? (underused / total) * 100 : 0; - if (!hasFeature(Feature.ALLOCATIONS_PAGE_IMPROVEMENTS) || true) return null; + if (!hasFeature(Feature.ALLOCATIONS_PAGE_IMPROVEMENTS) || Math.random() > -1) return null; return <> - - -
-

Key metrics settings

-

Select key metrics to display

-
- {/* + + +
+

Key metrics settings

+

Select key metrics to display

+
+ {/* Note(Louise): Leave this code disabled until we decide if it is needed or not
@@ -759,20 +759,20 @@ export const KeyMetrics: React.FunctionComponent<{
*/} +
+ + + {Object.values(settings).map(setting => ( + + ))} + + + + +
- - {Object.values(settings).map(setting => ( - - ))} - - - - - -
-
@@ -798,31 +798,31 @@ export const KeyMetrics: React.FunctionComponent<{

Sub-project allocations

{state.remoteData.wallets === undefined ? <> - : <> -
- {allocations.length !== 0 ? null : -
- You do not have given out allocated any resources at this time. - When you approve grant applications, the allocated resources will be shown here. -
} + : <> +
+ {allocations.length !== 0 ? null : +
+ You do not have given out allocated any resources at this time. + When you approve grant applications, the allocated resources will be shown here. +
} - {allocations.map(([rawType, tree]) => { - const type = rawType as ProductType; + {allocations.map(([rawType, tree]) => { + const type = rawType as ProductType; - return - - {Accounting.productAreaTitle(type)} - - } - right={ - {tree.usageAndQuota.map((uq, idx) => { - let label = `${okPercentage.toFixed(2)}% Ok` + - ` | ${atRiskPercentage.toFixed(2)}% At risk` + - ` | ${underusedPercentage.toFixed(2)}% Underused`; - return + return + + {Accounting.productAreaTitle(type)} + + } + right={ + {tree.usageAndQuota.map((uq, idx) => { + let label = `${okPercentage.toFixed(2)}% Ok` + + ` | ${atRiskPercentage.toFixed(2)}% At risk` + + ` | ${underusedPercentage.toFixed(2)}% Underused`; + return ; } - )} - } - indent={indent} - > - {tree.wallets.map((wallet, idx) => - - - {wallet.category.name} - - } - right={ - + )} } + indent={indent} > - - )} - ; + {tree.wallets.map((wallet, idx) => + + + {wallet.category.name} + + } + right={ + + } + > + + )} + ; })}
@@ -912,7 +912,7 @@ const FilteredUsageAndQuota: React.FunctionComponent<{ return <> {filteredEntries.map((uq, idx) => { if (idx > 2) return null; - return ; + return ; })} } @@ -1114,8 +1114,8 @@ const SubProjectListRow: React.FunctionComponent<{ } right={
- - + +
} onActivate={open => { if (open) setNodeState(TreeAction.OPEN, workspaceId, g.category.name); @@ -1290,7 +1290,7 @@ export const SubProjectFilters: React.FunctionComponent<{ }, []); useEffect(() => { - dispatchEvent({ type: "SubProjectFilterSettingsLoad", settings: subProjectsDefaultSettings }); + dispatchEvent({type: "SubProjectFilterSettingsLoad", settings: subProjectsDefaultSettings}); }, []); const [ascending, setAscending] = useState(true); @@ -1346,7 +1346,7 @@ export const SubProjectFilters: React.FunctionComponent<{ state={state} /> : null ))} - +

Sort by

@@ -1373,7 +1373,7 @@ export const SubProjectFilters: React.FunctionComponent<{ + onClick={onSortingToggle} />
@@ -1381,7 +1381,7 @@ export const SubProjectFilters: React.FunctionComponent<{ + zIndex={10000} gap={"8px"}> diff --git a/frontend-web/webclient/app/Accounting/Allocations/ProviderOnlySections.tsx b/frontend-web/webclient/app/Accounting/Allocations/ProviderOnlySections.tsx index feeeb71705..988b27c961 100644 --- a/frontend-web/webclient/app/Accounting/Allocations/ProviderOnlySections.tsx +++ b/frontend-web/webclient/app/Accounting/Allocations/ProviderOnlySections.tsx @@ -13,7 +13,7 @@ import {removePrefixFrom} from "@/Utilities/TextUtilities"; import {callAPI} from "@/Authentication/DataHook"; import * as Gifts from "@/Accounting/Gifts"; import {Client} from "@/Authentication/HttpClientInstance"; -import {sendFailureNotification, sendNotification, sendSuccessNotification, SnackType} from "@/Notifications"; +import {sendFailureNotification, sendSuccessNotification} from "@/Notifications"; const wayfIdpsPairs = WAYF.wayfIdps.map(it => ({value: it, content: it})); @@ -116,7 +116,7 @@ export const ProviderOnlySections: React.FunctionComponent<{ sendSuccessNotification("Root allocation has been created"); dispatchEvent({type: "ResetRootAllocation"}); dispatchEvent({type: "Init"}); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to create root allocation: " + extractErrorMessage(e)); return; } finally { @@ -254,7 +254,7 @@ export const ProviderOnlySections: React.FunctionComponent<{ gift.id = id; dispatchEvent({type: "GiftCreated", gift}); sendSuccessNotification("Gift Created"); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to create a gift: " + extractErrorMessage(e)); } finally { creatingGift.current = false; @@ -268,7 +268,7 @@ export const ProviderOnlySections: React.FunctionComponent<{ try { await callAPI(Gifts.remove({giftId: id})); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to delete gift: " + extractErrorMessage(e)); return; } diff --git a/frontend-web/webclient/app/Admin/Providers/Browse.tsx b/frontend-web/webclient/app/Admin/Providers/Browse.tsx index 5bbb52ac0f..d55092e286 100644 --- a/frontend-web/webclient/app/Admin/Providers/Browse.tsx +++ b/frontend-web/webclient/app/Admin/Providers/Browse.tsx @@ -12,6 +12,7 @@ import {ResourceBrowseCallbacks} from "@/UCloud/ResourceApi"; import AppRoutes from "@/Routes"; import {useSetRefreshFunction} from "@/Utilities/ReduxUtilities"; import {SidebarTabId} from "@/ui-components/SidebarComponents"; +import {Product} from "@/Accounting"; const defaultRetrieveFlags: {itemsPerPage: number} = { itemsPerPage: 250, @@ -133,7 +134,7 @@ function ProviderBrowse({opts}: {opts?: ResourceBrowserOpts}): React.R browser.on("pathToEntry", j => j.id); browser.on("fetchOperationsCallback", () => { const support = {productsByProvider: {}}; - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { api: ProvidersApi, navigate: to => navigate(to), commandLoading: false, diff --git a/frontend-web/webclient/app/Admin/SupportPage.tsx b/frontend-web/webclient/app/Admin/SupportPage.tsx index 8ed8ee7a9c..f1d707381a 100644 --- a/frontend-web/webclient/app/Admin/SupportPage.tsx +++ b/frontend-web/webclient/app/Admin/SupportPage.tsx @@ -206,7 +206,7 @@ export function UserSupportContent() { actionText={"Reset 2FA"} icon={"refresh"} color="errorMain" - onAction={() => { + onAction={async () => { const requiredText = it.username dialogStore.addDialog((
e.stopPropagation()}> diff --git a/frontend-web/webclient/app/Admin/UserCreation.tsx b/frontend-web/webclient/app/Admin/UserCreation.tsx index dc3c5ea9af..958c2a13d6 100644 --- a/frontend-web/webclient/app/Admin/UserCreation.tsx +++ b/frontend-web/webclient/app/Admin/UserCreation.tsx @@ -1,5 +1,5 @@ import {Client} from "@/Authentication/HttpClientInstance"; -import {setLoading, usePage} from "@/Navigation/Redux"; +import {setStatusLoading, usePage} from "@/Navigation/Redux"; import {usePromiseKeeper} from "@/PromiseKeeper"; import * as React from "react"; import {Button, Input, Label, MainContainer} from "@/ui-components"; @@ -194,21 +194,21 @@ function UserCreation(): React.ReactNode { if (!hasUsernameError && !hasPasswordError && !hasEmailError && !hasFirstnamesError && !hasLastnameError) { try { - reduxDispatch(setLoading(true)); + reduxDispatch(setStatusLoading(true)); setSubmitted(true); await promiseKeeper.makeCancelable( Client.post("/auth/users/register", [{username, password, email, firstnames, lastname}], "") ).promise; sendSuccessNotification(`User '${username}' successfully created`); dispatch({type: "Reset", payload: {}}); - } catch (err) { + } catch (err: any) { const status = defaultErrorHandler(err); if (status === 409) dispatch({ type: "UpdateErrors", payload: {usernameError: true, passwordError: false, emailError: false, firstnamesError: false, lastnameError: false} }); } finally { - reduxDispatch(setLoading(false)); + reduxDispatch(setStatusLoading(false)); setSubmitted(false); } } diff --git a/frontend-web/webclient/app/Applications/Branding/AutomaticBranding.tsx b/frontend-web/webclient/app/Applications/Branding/AutomaticBranding.tsx index 3678cf185e..984381d109 100644 --- a/frontend-web/webclient/app/Applications/Branding/AutomaticBranding.tsx +++ b/frontend-web/webclient/app/Applications/Branding/AutomaticBranding.tsx @@ -2,56 +2,50 @@ import * as React from "react"; import {useCloudAPI} from "@/Authentication/DataHook"; import {brandingApi, BrandingResponse, BrandingLoginPageType} from "@/UCloud/BrandingApi"; import {useDispatch} from "react-redux"; -import {PayloadAction} from "@reduxjs/toolkit"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; export const AutomaticBranding: React.FunctionComponent = () => { - const [branding, fetchBranding] = useCloudAPI( - brandingApi.retrieve(), - { - deploymentName: "", - loginPage: { type: BrandingLoginPageType.DEIC, primaryLogoUrl: "", secondaryLogoUrls: [] }, - }, - ); - - React.useEffect(() => { - const intervalId = setInterval(() => { - fetchBranding(brandingApi.retrieve()); - }, 1000 * 60 * 60); - return () => { - clearInterval(intervalId); - }; - }, []); - - const dispatch = useDispatch(); - - React.useEffect(() => { - dispatch({ type: ADD_BRANDING, payload: branding.data }); - }, [branding.data]); - - return null; + const [branding, fetchBranding] = useCloudAPI( + brandingApi.retrieve(), + { + deploymentName: "", + loginPage: {type: BrandingLoginPageType.DEIC, primaryLogoUrl: "", secondaryLogoUrls: []}, + }, + ); + + React.useEffect(() => { + const intervalId = setInterval(() => { + fetchBranding(brandingApi.retrieve()); + }, 1000 * 60 * 60); + return () => { + clearInterval(intervalId); + }; + }, []); + const dispatch = useDispatch(); + + React.useEffect(() => { + dispatch(addBranding(branding.data)); + }, [branding.data]); + + return null; }; -const ADD_BRANDING = "ADD_BRANDING"; -type SetBranding = PayloadAction; - -type BrandingAction = SetBranding; - export function initBranding(): BrandingResponse { - return { - deploymentName: "", - loginPage: { type: BrandingLoginPageType.DEIC, primaryLogoUrl: "", secondaryLogoUrls: [] }, - }; + return { + deploymentName: "", + loginPage: {type: BrandingLoginPageType.DEIC, primaryLogoUrl: "", secondaryLogoUrls: []}, + }; } -export function brandingReducer( - state: BrandingResponse = initBranding(), - action: BrandingAction, -): BrandingResponse { - switch (action.type) { - case ADD_BRANDING: { - return action.payload; +const brandingSlice = createSlice({ + name: "branding", + initialState: initBranding(), + reducers: { + addBranding(state, action: PayloadAction) { + state = action.payload; + } } - default: - return state; - } -} +}) + +export const {addBranding} = brandingSlice.actions; +export const brandingReducer = brandingSlice.reducer; \ No newline at end of file diff --git a/frontend-web/webclient/app/Applications/FavoriteToggle.tsx b/frontend-web/webclient/app/Applications/FavoriteToggle.tsx index a0fccd39d3..4b8e3e4227 100644 --- a/frontend-web/webclient/app/Applications/FavoriteToggle.tsx +++ b/frontend-web/webclient/app/Applications/FavoriteToggle.tsx @@ -3,10 +3,9 @@ import {useCallback, useEffect, useState} from "react"; import {useCloudCommand} from "@/Authentication/DataHook"; import {Icon} from "@/ui-components"; import {useDispatch} from "react-redux"; -import {toggleAppFavorite} from "./Redux/Actions"; -import {Application, ApplicationWithFavoriteAndTags} from "@/Applications/AppStoreApi"; +import {Application} from "@/Applications/AppStoreApi"; import * as AppStore from "@/Applications/AppStoreApi"; -import {useIsLightThemeStored} from "@/ui-components/theme"; +import {toggleAppFavorite} from "./Redux/Reducer"; export const FavoriteToggle: React.FunctionComponent<{ application: Application @@ -14,7 +13,6 @@ export const FavoriteToggle: React.FunctionComponent<{ const [loading, invokeCommand] = useCloudCommand(); const [favorite, setFavorite] = useState(application.favorite); const dispatch = useDispatch(); - const lightTheme = useIsLightThemeStored(); useEffect(() => { setFavorite(application.favorite); }, [application]); @@ -22,7 +20,7 @@ export const FavoriteToggle: React.FunctionComponent<{ const toggle = useCallback(async () => { if (!loading) { setFavorite(!favorite); - dispatch(toggleAppFavorite(application, !favorite)); + dispatch(toggleAppFavorite({app: application, favorite: !favorite})); invokeCommand(AppStore.toggleStar({ name: application.metadata.name })); diff --git a/frontend-web/webclient/app/Applications/Jobs/CatalogDiscoveryMode.tsx b/frontend-web/webclient/app/Applications/Jobs/CatalogDiscoveryMode.tsx index d63f55231c..a86e54ba1a 100644 --- a/frontend-web/webclient/app/Applications/Jobs/CatalogDiscoveryMode.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/CatalogDiscoveryMode.tsx @@ -63,7 +63,7 @@ export const CatalogDiscoveryModeSwitcher: React.FunctionComponent = () => { closeFn.current(); }, []); - const onKeyboardSelect = useCallback((el: HTMLElement) => { + const onKeyboardSelect = useCallback((el?: Element) => { const mode = el?.getAttribute("data-mode"); if (mode) onModeChange(mode); }, [onModeChange]); diff --git a/frontend-web/webclient/app/Applications/Jobs/Create.tsx b/frontend-web/webclient/app/Applications/Jobs/Create.tsx index 9d38884df3..f69dd04e47 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Create.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Create.tsx @@ -624,7 +624,7 @@ export const Create: React.FunctionComponent = () => { } navigate(`/jobs/properties/${ids[0]?.id}?app=${application.metadata.name}`); - } catch (e) { + } catch (e: any) { const code = extractErrorCode(e); if (code === 409) { addStandardDialog({ diff --git a/frontend-web/webclient/app/Applications/Jobs/JobViz/Renderer.tsx b/frontend-web/webclient/app/Applications/Jobs/JobViz/Renderer.tsx index 6a5dd07b25..eca4aeefe8 100644 --- a/frontend-web/webclient/app/Applications/Jobs/JobViz/Renderer.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/JobViz/Renderer.tsx @@ -61,7 +61,8 @@ export const Renderer: React.FunctionComponent<{ }, []); useEffect(() => { - const listeners: ((ev: unknown) => void)[] = []; + /* TODO(Jonas): Different type */ + const listeners: ((ev: any) => void)[] = []; listeners.push(props.processor.on("appendRow", ({row, channel}) => { const state = stateRef.current; @@ -184,7 +185,8 @@ export const Renderer: React.FunctionComponent<{ return () => { for (const l of listeners) { - props.processor.removeListener(l); + // Note(Jonas): First arg is just to make the compiler stop complaining. It's not used. + props.processor.removeListener("kvPropertiesUpdated", l); } }; }, [props.processor]); @@ -254,7 +256,7 @@ export const Renderer: React.FunctionComponent<{ if (placedInContainer[w.id] || w.id.startsWith("anon-")) { return null; } else { - return ; + return ; } })} ; @@ -276,17 +278,17 @@ const RendererWidget: React.FunctionComponent<{ }> = ({widget, state}) => { switch (widget.type) { case WidgetType.WidgetTypeLabel: - return }/>; + return } />; case WidgetType.WidgetTypeProgressBar: - return }/>; + return } />; case WidgetType.WidgetTypeTable: - return }/>; + return } />; case WidgetType.Tombstone1: return null; case WidgetType.WidgetTypeLineChart: - return }/>; + return } />; case WidgetType.WidgetTypeContainer: - return } state={state}/>; + return } state={state} />; case WidgetType.WidgetTypeSnippet: return } />; } @@ -334,7 +336,7 @@ function transformToSSHUrl(command?: string | null): `ssh://${string}:${number}` return `ssh://${hostname}:${portNumber}`; } -const RendererSnippet: React.FunctionComponent<{ widget: RuntimeWidget }> = ({widget}) => { +const RendererSnippet: React.FunctionComponent<{widget: RuntimeWidget}> = ({widget}) => { const sshUrl = React.useMemo(() => transformToSSHUrl(widget.spec.text), [widget.spec.text]); const body = {widget.spec.text}; if (sshUrl != null) { @@ -344,11 +346,11 @@ const RendererSnippet: React.FunctionComponent<{ widget: RuntimeWidget }> = ({widget}) => { +const RendererLabel: React.FunctionComponent<{widget: RuntimeWidget}> = ({widget}) => { return
{widget.spec.text}
; }; -const RendererProgressBar: React.FunctionComponent<{ widget: RuntimeWidget }> = ({widget}) => { +const RendererProgressBar: React.FunctionComponent<{widget: RuntimeWidget}> = ({widget}) => { return ; }; -const RendererTable: React.FunctionComponent<{ widget: RuntimeWidget }> = ({widget}) => { +const RendererTable: React.FunctionComponent<{widget: RuntimeWidget}> = ({widget}) => { return {widget.spec.rows.map((row, idx) => { const renderedRow = {row.cells.map((cell, cellIdx) => { - const label = ; + const label = ; if ((cell.flags & WidgetTableCellFlag.WidgetTableCellHeader) != 0) { return {label}; @@ -396,7 +398,7 @@ function dummyWidget(type: WidgetType, spec: K): RuntimeWidget { }; } -const RendererDiagram: React.FunctionComponent<{ widget: RuntimeWidget }> = ({widget}) => { +const RendererDiagram: React.FunctionComponent<{widget: RuntimeWidget}> = ({widget}) => { const labelFormatter = useCallback((value: number, isAxis: boolean): string => { switch (widget.spec.yAxis.unit) { case WidgetDiagramUnit.GenericInt: { @@ -448,7 +450,7 @@ const RendererDiagram: React.FunctionComponent<{ widget: RuntimeWidget; + return ; } else if (child.id != null) { const resolvedChild = state[child.id.id]; if (resolvedChild) { - return ; + return ; } } return null; diff --git a/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts b/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts index 4995ec69f1..e602056eff 100644 --- a/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts +++ b/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts @@ -43,10 +43,10 @@ export function useJobVizProperties(processor: StreamProcessor): Record { const listener = processor.on("kvPropertiesUpdated", ({newProperties}) => { setProperties(newProperties); - }); + }) return () => { - processor.removeListener(listener); + processor.removeListener("kvPropertiesUpdated", listener); }; }, [processor]); @@ -109,7 +109,11 @@ export class StreamProcessor { return listener; } - removeListener(listener: (ev: EventMap[K]) => void) { + /* + Note(Jonas): Unused `type` seems to be need for the function to understand what listener we are providing. + And it seems any one will do???? + */ + removeListener(_: K, listener: (ev: EventMap[K]) => void) { this.listeners = this.listeners.filter(([, itListener]) => itListener !== listener); } diff --git a/frontend-web/webclient/app/Applications/Jobs/JobsBrowse.tsx b/frontend-web/webclient/app/Applications/Jobs/JobsBrowse.tsx index 8b2ebdbe46..5b22bbf2e3 100644 --- a/frontend-web/webclient/app/Applications/Jobs/JobsBrowse.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/JobsBrowse.tsx @@ -51,6 +51,7 @@ import {SimpleAvatarComponentCache} from "@/Files/Shares"; import {divText} from "@/Utilities/HTMLUtilities"; import {TruncateClass} from "@/ui-components/Truncate"; import {sendFailureNotification} from "@/Notifications"; +import {ProductCompute} from "@/Accounting"; const defaultRetrieveFlags: {itemsPerPage: number} = { itemsPerPage: 250, @@ -72,7 +73,7 @@ const simpleViewColumnTitles: ColumnTitleList = [{name: ""}, {name: "", sortById const RESOURCE_NAME = "JOBS"; -function JobBrowse({opts}: {opts?: ResourceBrowserOpts & {omitBreadcrumbs?: boolean; operations?: Operation>[]; jobTypeFilter?: JobTypeFilter}}): React.ReactNode { +function JobBrowse({opts}: {opts?: ResourceBrowserOpts & {omitBreadcrumbs?: boolean; operations?: Operation>[]; jobTypeFilter?: JobTypeFilter}}): React.ReactNode { const mountRef = React.useRef(null); const browserRef = React.useRef | null>(null); const navigate = useNavigate(); @@ -355,7 +356,7 @@ function JobBrowse({opts}: {opts?: ResourceBrowserOpts & {omitBreadcrumbs?: browser.on("pathToEntry", j => j.id); browser.on("fetchOperationsCallback", () => { const support = {productsByProvider: {}}; - const callbacks: ResourceBrowseCallbacks & {isModal: boolean} = { + const callbacks: ResourceBrowseCallbacks = { api: JobsApi, navigate: to => navigate(to), commandLoading: false, diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/Folders.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/Folders.tsx index b82f837ee7..09c3c9425f 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/Folders.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/Folders.tsx @@ -9,6 +9,7 @@ import {anyFolderDuplicates} from "../Widgets/GenericFiles"; import {Application, ApplicationParameter} from "@/Applications/AppStoreApi"; import AppRoutes from "@/Routes"; import {doNothing} from "@/UtilityFunctions"; +import {Dispatch, SetStateAction} from "react"; export function folderResourceAllowed(app: Application): boolean { if (app.invocation.allowAdditionalMounts != null) return app.invocation.allowAdditionalMounts; @@ -25,7 +26,7 @@ export const FolderResource: React.FunctionComponent<{ application: Application; params: ApplicationParameter[]; errors: Record; - setErrors: (errors: Record) => void; + setErrors: Dispatch>>; warning: string; setWarning: (warning: string) => void; onAdd: () => void; diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/Ingress.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/Ingress.tsx index df7a1bde31..a526481908 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/Ingress.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/Ingress.tsx @@ -11,6 +11,7 @@ import * as Heading from "@/ui-components/Heading"; import BaseLink from "@/ui-components/BaseLink"; import {Application, ApplicationParameter} from "@/Applications/AppStoreApi"; import {doNothing} from "@/UtilityFunctions"; +import {Dispatch, SetStateAction} from "react"; export function ingressResourceAllowed(app: Application, bindLinkToPort = false): boolean { if (app.invocation.allowPublicLink === false) return false; @@ -26,7 +27,7 @@ export const IngressResource: React.FunctionComponent<{ onAdd: () => void; onRemove: (id: string) => void; provider?: string; - setErrors: (errors: Record) => void; + setErrors: Dispatch>>; }> = ({application, bindLinkToPort, params, errors, onAdd, onRemove, provider, setErrors}) => { if (!ingressResourceAllowed(application, bindLinkToPort)) return null; diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/NetworkIPs.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/NetworkIPs.tsx index 000dfa180f..f547fe8220 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/NetworkIPs.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/NetworkIPs.tsx @@ -20,7 +20,7 @@ export const NetworkIPResource: React.FunctionComponent<{ application: Application; params: ApplicationParameter[]; errors: Record; - setErrors: (errors: Record) => void; + setErrors: React.Dispatch>> onAdd: () => void; onRemove: (id: string) => void; provider?: string; diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/Peers.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/Peers.tsx index 4306cede35..c7295c0c5f 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/Peers.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/Peers.tsx @@ -6,6 +6,7 @@ import {Widget} from "@/Applications/Jobs/Widgets"; import {Application, ApplicationParameter} from "@/Applications/AppStoreApi"; import {doNothing} from "@/UtilityFunctions"; import {Feature, hasFeature} from "@/Features"; +import {Dispatch, SetStateAction} from "react"; export function peerResourceAllowed(app: Application) { const invocation = app.invocation; @@ -18,7 +19,7 @@ export const PeerResource: React.FunctionComponent<{ application: Application; params: ApplicationParameter[]; errors: Record; - setErrors: (errors: Record) => void; + setErrors: Dispatch>> onAdd: () => void; onRemove: (id: string) => void; }> = ({application, params, errors, onAdd, onRemove, setErrors}) => { diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx index cce43d0149..cc30b3c412 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx @@ -14,7 +14,7 @@ export const PrivateNetworkResource: React.FunctionComponent<{ application: Application; params: ApplicationParameter[]; errors: Record; - setErrors: (errors: Record) => void; + setErrors: React.Dispatch>> onAdd: () => void; onRemove: (id: string) => void; provider?: string; @@ -31,72 +31,72 @@ export const PrivateNetworkResource: React.FunctionComponent<{ dnsHostname, onDnsHostnameChange, }) => { - if (!peerResourceAllowed(application) || !hasFeature(Feature.NEW_VM_UI)) return null; + if (!peerResourceAllowed(application) || !hasFeature(Feature.NEW_VM_UI)) return null; - return ( - - - - - Connect to other jobs + return ( + + + + + Connect to other jobs + + + + + + {params.length !== 0 ? ( + + +
+ Your job will be identified by this name within the network. +
+
+ ) : ( + <> + If you need to connect this job to a network of other jobs then click {" "} + { + e.preventDefault(); + onAdd(); + }} + > + "Connect network" + + {" "} + to select one. You can manage networks in {" "} + + private networks + + . + + )}
- -
- - {params.length !== 0 ? ( - - -
- Your job will be identified by this name within the network. -
-
- ) : ( - <> - If you need to connect this job to a network of other jobs then click {" "} - { - e.preventDefault(); - onAdd(); + {params.map(entry => ( + + { + onRemove(entry.name); }} - > - "Connect network" - - {" "} - to select one. You can manage networks in {" "} - - private networks - - . - - )} + /> +
+ ))}
- - {params.map(entry => ( - - { - onRemove(entry.name); - }} - /> - - ))} - -
- ); -}; + + ); + }; diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/index.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/index.tsx index d2050215a7..2336b869fb 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/index.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from "react"; +import {Dispatch, SetStateAction, useCallback, useState} from "react"; import {setWidgetValues} from "@/Applications/Jobs/Widgets"; import {flushSync} from "react-dom"; import {ApplicationParameter} from "@/Applications/AppStoreApi"; @@ -12,7 +12,7 @@ export interface ResourceHook { params: ApplicationParameter[]; errors: Record; provider?: string; - setErrors: (newErrors: Record) => void; + setErrors: Dispatch>>; warning: string; setWarning: (warning: string) => void; } diff --git a/frontend-web/webclient/app/Applications/Jobs/View.tsx b/frontend-web/webclient/app/Applications/Jobs/View.tsx index 0a3e468b1d..9def30f132 100644 --- a/frontend-web/webclient/app/Applications/Jobs/View.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/View.tsx @@ -1079,7 +1079,7 @@ const RunningContent: React.FunctionComponent<{ }, [status.expiresAt]); - const suspendJob = React.useCallback(() => { + const suspendJob = React.useCallback(async () => { try { setSuspended(true); invokeCommand(JobsApi.suspend(bulkRequestOf({id: job.id}))); diff --git a/frontend-web/webclient/app/Applications/Jobs/Widgets/Machines.tsx b/frontend-web/webclient/app/Applications/Jobs/Widgets/Machines.tsx index 5227f4fe28..4f5b15730d 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Widgets/Machines.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Widgets/Machines.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import {useEffect, useMemo, useState} from "react"; import {accounting, compute} from "@/UCloud"; import ComputeProductReference = accounting.ProductReference; -import {ProductV2, productCategoryEquals, ProductV2Compute, ProductCompute, WalletV2} from "@/Accounting"; +import {ProductV2, productCategoryEquals, ProductV2Compute, ProductCompute, WalletV2, Product} from "@/Accounting"; import {ProductSelector} from "@/Products/Selector"; import {ResolvedSupport} from "@/UCloud/ResourceApi"; import JobsRetrieveProductsResponse = compute.JobsRetrieveProductsResponse; @@ -16,9 +16,8 @@ export function findRelevantMachinesForApplication( computeProducts: ProductV2Compute[], wallets: WalletV2[] ): ProductV2Compute[] { - const supportedProducts: ProductCompute[] = ([] as ProductCompute[]).concat.apply( - [], - Object.values(machineSupport.productsByProvider).map(products => + const supportedProducts: ProductCompute[] = + Object.values(machineSupport.productsByProvider).flatMap(products => products .filter(it => { const tool = application.invocation.tool.tool!; @@ -44,9 +43,8 @@ export function findRelevantMachinesForApplication( .filter(product => computeProducts.some(wallet => productCategoryEquals(product.product.category, wallet.category)) ) - .map(it => it.product) - ) - ); + .map(it => it.product as unknown as ProductCompute) + ); const result: ProductV2Compute[] = []; diff --git a/frontend-web/webclient/app/Applications/Jobs/Widgets/ModuleList.tsx b/frontend-web/webclient/app/Applications/Jobs/Widgets/ModuleList.tsx index 49b18d7134..f18ccb0402 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Widgets/ModuleList.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Widgets/ModuleList.tsx @@ -458,13 +458,13 @@ export const ModuleListParameter: React.FunctionComponent = pro export function ModuleMarkdown({children}: React.PropsWithChildren): React.ReactNode { return {p.children}, + h1: p => , + h2: p => , + h3: p => , + h4: p => , + h5: p => , + h6: p => , }} allowedElements={["h1", "h2", "h3", "h4", "h5", "h6", "br", "a", "p", "strong", "b", "i", "em", "ul", "ol", "li"]} children={children as string} @@ -472,11 +472,11 @@ export function ModuleMarkdown({children}: React.PropsWithChildren): React.React /> } -function LinkBlock(props: {href?: string; children: React.ReactNode & React.ReactNode[]}) { +function LinkBlock(props: {href?: string; children: React.ReactNode}) { return {props.children}; } -function SimpleHeading(props: {children: React.ReactNode & React.ReactNode[]}) { +function SimpleHeading(props: {children: React.ReactNode}) { return {props.children}; } diff --git a/frontend-web/webclient/app/Applications/LicenseBrowse.tsx b/frontend-web/webclient/app/Applications/LicenseBrowse.tsx index 7285a0e175..894ac57a50 100644 --- a/frontend-web/webclient/app/Applications/LicenseBrowse.tsx +++ b/frontend-web/webclient/app/Applications/LicenseBrowse.tsx @@ -21,7 +21,7 @@ import { import {doNothing, extractErrorMessage} from "@/UtilityFunctions"; import AppRoutes from "@/Routes"; import {AsyncCache} from "@/Utilities/AsyncCache"; -import {productTypeToIcon, ProductV2License} from "@/Accounting"; +import {ProductLicense, productTypeToIcon, ProductV2License} from "@/Accounting"; import {bulkRequestOf} from "@/UtilityFunctions"; import {FindByStringId} from "@/UCloud"; import {useSetRefreshFunction} from "@/Utilities/ReduxUtilities"; @@ -208,7 +208,7 @@ export function LicenseBrowse({ browser.setEmptyIcon(productTypeToIcon("LICENSE")); browser.on("fetchOperationsCallback", () => { - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { supportByProvider: supportByProvider.retrieveFromCacheOnly("") ?? {productsByProvider: {}}, dispatch, isWorkspaceAdmin: checkIsWorkspaceAdmin(), @@ -300,7 +300,7 @@ export function LicenseBrowse({ dialogStore.success(); browser.refresh(); } - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to create license. " + extractErrorMessage(e)); browser.refresh(); return; diff --git a/frontend-web/webclient/app/Applications/NetworkIP/NetworkIPBrowse.tsx b/frontend-web/webclient/app/Applications/NetworkIP/NetworkIPBrowse.tsx index 0d38e0f5fc..ac3a71852e 100644 --- a/frontend-web/webclient/app/Applications/NetworkIP/NetworkIPBrowse.tsx +++ b/frontend-web/webclient/app/Applications/NetworkIP/NetworkIPBrowse.tsx @@ -1,4 +1,4 @@ -import {productTypeToIcon, ProductV2, ProductV2NetworkIP} from "@/Accounting"; +import {ProductNetworkIP, productTypeToIcon, ProductV2, ProductV2NetworkIP} from "@/Accounting"; import {callAPI} from "@/Authentication/DataHook"; import {bulkRequestOf, displayErrorMessageOrDefault, extractErrorMessage, stopPropagation} from "@/UtilityFunctions"; import MainContainer from "@/ui-components/MainContainer"; @@ -214,7 +214,7 @@ export function NetworkIPBrowse({ }); browser.on("fetchOperationsCallback", () => { - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { supportByProvider: {productsByProvider: {}}, dispatch, isWorkspaceAdmin: checkIsWorkspaceAdmin(), @@ -234,7 +234,7 @@ export function NetworkIPBrowse({ browser.on("fetchOperations", () => { const entries = browser.findSelectedEntries(); - const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; + const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; const operations = NetworkIPApi.retrieveOperations(); const create = operations.find(it => it.tag === CREATE_TAG); @@ -311,7 +311,7 @@ export function NetworkIPBrowse({ dialogStore.success(); browser.refresh(); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to activate public IP. " + extractErrorMessage(e)); browser.refresh(); return; diff --git a/frontend-web/webclient/app/Applications/PrivateNetwork/PrivateNetworkBrowse.tsx b/frontend-web/webclient/app/Applications/PrivateNetwork/PrivateNetworkBrowse.tsx index 21ea098a73..411f23283d 100644 --- a/frontend-web/webclient/app/Applications/PrivateNetwork/PrivateNetworkBrowse.tsx +++ b/frontend-web/webclient/app/Applications/PrivateNetwork/PrivateNetworkBrowse.tsx @@ -38,7 +38,7 @@ import {isAdminOrPI} from "@/Project"; import {PermissionsTable} from "@/Resource/PermissionEditor"; import {useProject} from "@/Project/cache"; import {useProjectId} from "@/Project/Api"; -import {productTypeToIcon, ProductV2, ProductV2PrivateNetwork} from "@/Accounting"; +import {Product, productTypeToIcon, ProductV2, ProductV2PrivateNetwork} from "@/Accounting"; import {ProductSelector} from "@/Products/Selector"; import {Client} from "@/Authentication/HttpClientInstance"; import {AsyncCache} from "@/Utilities/AsyncCache"; @@ -206,7 +206,7 @@ export function PrivateNetworkBrowse({ }); browser.on("fetchOperationsCallback", () => { - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { supportByProvider: {productsByProvider: {}}, dispatch, isWorkspaceAdmin: checkIsWorkspaceAdmin(), @@ -226,7 +226,7 @@ export function PrivateNetworkBrowse({ browser.on("fetchOperations", () => { const entries = browser.findSelectedEntries(); - const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; + const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; const operations = PrivateNetworkApi.retrieveOperations(); const create = operations.find(it => it.tag === CREATE_TAG); @@ -291,7 +291,7 @@ export function PrivateNetworkBrowse({ dialogStore.success(); browser.refresh(); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to create private network. " + extractErrorMessage(e)); browser.refresh(); } @@ -337,7 +337,7 @@ export function PrivateNetworkBrowse({ return -
+
{headerControls?.projectSwitcherTarget ? createProjectSwitcherPortal(headerControls.projectSwitcherTarget) : switcher} @@ -395,7 +395,7 @@ function PrivateNetworkCreate({onCreate, onCancel, products}: PrivateNetworkCrea - + - + - - + +
This network can be used with machines from {shortProviderId}.
@@ -459,7 +459,7 @@ function PrivateNetworkCreate({onCreate, onCancel, products}: PrivateNetworkCrea
+ gap={"8px"}> - {categories.length < 1 ? <>No categories here: <> + {categories.length < 1 ? <>No categories here : <> {categories.map((c, idx) => { return { await refresh(); }, [id, refresh]); - const removeCategory = useCallback(async (key: string) => { + const removeCategory = useCallback(async (key?: string | undefined) => { + if (!key) return; const categoryId = parseInt(key); await callAPI(AppStore.removeGroupFromCategory({ groupId: id, diff --git a/frontend-web/webclient/app/Applications/Studio/Groups.tsx b/frontend-web/webclient/app/Applications/Studio/Groups.tsx index 53eba59619..f7e892ab11 100644 --- a/frontend-web/webclient/app/Applications/Studio/Groups.tsx +++ b/frontend-web/webclient/app/Applications/Studio/Groups.tsx @@ -165,7 +165,7 @@ export const ApplicationGroups: React.FunctionComponent = () => { } right={ - { + { invokeCommand(AppStore.deleteGroup({id: group.metadata.id})).then(doNothing); fetchGroups(); }} icon="heroTrash" /> diff --git a/frontend-web/webclient/app/Applications/Studio/SpotlightsEditor.tsx b/frontend-web/webclient/app/Applications/Studio/SpotlightsEditor.tsx index b22496333e..2a309f0d2f 100644 --- a/frontend-web/webclient/app/Applications/Studio/SpotlightsEditor.tsx +++ b/frontend-web/webclient/app/Applications/Studio/SpotlightsEditor.tsx @@ -34,7 +34,7 @@ const SpotlightForm: ScaffoldedFormObject = { label: "Title", placeholder: "Artificial intelligence", help: "This is the title which will be displayed in the card's title.", - validator: (t: string) => { + validator: (t) => { if (!t) return "Title cannot be empty"; return null; } @@ -45,7 +45,7 @@ const SpotlightForm: ScaffoldedFormObject = { label: "Description", help: "This is the text which will be shown next to the applications. Use it to motivate why these applications are interesting.", rows: 12, - validator: (t: string) => { + validator: (t) => { if (!t) return "Description cannot be empty"; return null; } diff --git a/frontend-web/webclient/app/Authentication/lib.ts b/frontend-web/webclient/app/Authentication/lib.ts index 38a67d8262..439ff5b21f 100644 --- a/frontend-web/webclient/app/Authentication/lib.ts +++ b/frontend-web/webclient/app/Authentication/lib.ts @@ -215,7 +215,7 @@ export class HttpClient { req.send(); } }); - } catch (e) { + } catch (e: any) { console.warn(e); if (!this.isPublicPage) { if (nextAllowedFailureNotificationTS < new Date().getTime()) { @@ -411,7 +411,7 @@ export class HttpClient { } else { reject(req.response); } - } catch (e) { + } catch (e: any) { reject(e.response); } }; @@ -474,7 +474,7 @@ export class HttpClient { } reject({status: req.status, response: req.response}); } - } catch (e) { + } catch (e: any) { reject({status: e.status, response: e.response}); } }; @@ -546,7 +546,7 @@ export class HttpClient { return; } throw Error("The server was unreachable, please try again later."); - } catch (err) { + } catch (err: any) { sendFailureNotification(err.message); } } diff --git a/frontend-web/webclient/app/Dashboard/Dashboard.tsx b/frontend-web/webclient/app/Dashboard/Dashboard.tsx index a08da0ff74..0029924041 100644 --- a/frontend-web/webclient/app/Dashboard/Dashboard.tsx +++ b/frontend-web/webclient/app/Dashboard/Dashboard.tsx @@ -92,10 +92,16 @@ function Dashboard(): React.ReactNode { useSetRefreshFunction(reload); const main = (
- + + + + - +
@@ -129,11 +135,11 @@ const GridClass = injectStyle("grid", k => ` margin-bottom: 24px; gap: 24px; } -} +} @media screen and (max-width: 1260px) { ${k} > * { margin-bottom: 24px; - } + } ${k} > *:first-child { margin-top: 24px; } @@ -141,8 +147,8 @@ const GridClass = injectStyle("grid", k => ` `); function Invites({projectReloadRef, inviteReloadRef}: { - projectReloadRef: React.RefObject<() => void>, - inviteReloadRef: React.RefObject<() => void> + projectReloadRef: React.RefObject<() => void>; + inviteReloadRef: React.RefObject<() => void>; }): React.ReactNode { const [showProjectInvites, setShowProjectInvites] = React.useState(true); const [showShareInvites, setShowShareInvites] = React.useState(true); @@ -151,30 +157,37 @@ function Invites({projectReloadRef, inviteReloadRef}: { // HACK(Jonas): Hacky approach to ensure that --rowWidth is correctly set on initial mount. setShowProjectInvites(false); setShowShareInvites(false); - }, []) + }, []); - return + return ( -
-
+
+ +
+
+ +
-
+
); function display(val: boolean): {display: "none" | undefined} { - return {display: val ? undefined : "none"} + return {display: val ? undefined : "none"}; } } @@ -192,30 +205,37 @@ export function newsRequest(payload: NewsRequestProps): APICallParameters void>}): React.ReactNode { - return - - ; + + ); } function ApplyLinkButton(): React.ReactNode { const project = useProject(); const canApply = !Client.hasActiveProject || isAdminOrPI(project.fetch().status.myRole); - if (!canApply) return
+ if (!canApply) return
; - return + return ( - ; + ); } const ROW_HEIGHT_IN_PX = 55; @@ -276,7 +296,10 @@ function DashboardResources({wallets}: { - + {category.name} @@ -291,7 +314,9 @@ function DashboardResources({wallets}: {
- + + + } @@ -309,22 +334,24 @@ function DashboardGrantApplications({reloadRef}: {reloadRef: React.RefObject<() title="Grant applications" icon="heroDocumentCheck" > - + ; -}; +} function DashboardNews({news}: {news: APICallState>}): React.ReactNode { const lightTheme = useIsLightThemeStored(); - const newsItem = news.data.items.length > 0 ? news.data.items[0] : null; + const newsItem = news.data.items.at(0); return ( >}): React.Reac {newsItem.subtitle}} - right={{dateToString(newsItem.showFrom)}} + right={ + {dateToString(newsItem.showFrom)} + } /> {p.children}, }} unwrapDisallowed remarkPlugins={[remarkGfm]} @@ -362,22 +391,37 @@ function DashboardNews({news}: {news: APICallState>}): React.Reac }
- {onSandbox() ? <> - - {"Interreg"} - {"HALRIC"} + {onSandbox() ? ( + + {"Interreg"} + {"HALRIC"} - : <> - {"UCloud - } - + ) : {"UCloud}
- + ); } -function LinkBlock(props: {href?: string; children: React.ReactNode & React.ReactNode[]}) { - return {props.children}; +function LinkBlock(props: {href?: string; children: React.ReactNode}) { + return + {props.children} + ; } const NewsClass = injectStyle("with-graphic", k => ` @@ -385,7 +429,7 @@ const NewsClass = injectStyle("with-graphic", k => ` display: flex; height: 270px; } - + ${k}.halric { flex-wrap: wrap; height: unset; @@ -396,7 +440,7 @@ const NewsClass = injectStyle("with-graphic", k => ` flex-grow: 4; margin-right: 32px; } - + ${k} > div:nth-child(2) { flex-basis: 550px; flex-grow: 1; @@ -410,7 +454,7 @@ const NewsClass = injectStyle("with-graphic", k => ` position: relative; top: -112px; } - + ${k} h5 { margin: 0; margin-bottom: 10px; @@ -421,7 +465,7 @@ const NewsClass = injectStyle("with-graphic", k => ` display: none; width: 0px; } - + ${k} > div { width: 100%; } @@ -441,15 +485,23 @@ const DashboardCard: React.FunctionComponent<{ children: React.ReactNode; overflow?: string; }> = props => { - return {props.title} : - {props.title}} - icon={props.icon} - overflow={props.overflow} - > - {props.children} - -} + return ( + + + {props.title} + + : + {props.title} + } + icon={props.icon} + overflow={props.overflow} + > + {props.children} + + ); +}; export default Dashboard; diff --git a/frontend-web/webclient/app/Dashboard/Redux/index.tsx b/frontend-web/webclient/app/Dashboard/Redux/index.tsx index 650b6f9402..e04eb63028 100644 --- a/frontend-web/webclient/app/Dashboard/Redux/index.tsx +++ b/frontend-web/webclient/app/Dashboard/Redux/index.tsx @@ -1,36 +1,17 @@ -import {SetLoadingAction} from "@/Types"; -import {DashboardStateProps} from "@/Dashboard"; -import {initDashboard} from "@/DefaultObjects"; -import {PayloadAction} from "@reduxjs/toolkit"; +import {createSlice, PayloadAction} from '@reduxjs/toolkit' -export type Index = DashboardErrorAction | - SetLoadingAction; - -type DashboardError = - typeof DASHBOARD_RECENT_JOBS_ERROR; - -type DashboardErrorAction = PayloadAction<{error?: string}, DashboardError>; - -/** - * Sets all dashboard lists as either loading or not loading - * @param {loading} loading whether or not everything is loading or not - */ -export const setAllLoading = (loading: boolean): SetLoadingAction => ({ - type: SET_ALL_LOADING, - payload: {loading} -}); +export const dashboardSlice = createSlice({ + name: "dashboard", + initialState: { + loading: false, + }, + reducers: { + setAllLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + } + }, +}) -export const SET_ALL_LOADING = "SET_ALL_LOADING"; -export const DASHBOARD_RECENT_JOBS_ERROR = "DASHBOARD_RECENT_JOBS_ERROR"; +export const {setAllLoading} = dashboardSlice.actions -export function dashboardReducer(state: DashboardStateProps = initDashboard(), action: Index): DashboardStateProps { - switch (action.type) { - case SET_ALL_LOADING: { - const {loading} = action.payload; - return {...state, loading}; - } - default: { - return state; - } - } -} +export const dashboardReducer = dashboardSlice.reducer; \ No newline at end of file diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index 91c22390b2..eb03677323 100644 --- a/frontend-web/webclient/app/DefaultObjects.ts +++ b/frontend-web/webclient/app/DefaultObjects.ts @@ -30,7 +30,7 @@ export interface LegacyReduxObject { terminal: TerminalState; providerBrandings: ProviderBrandingResponse; branding: BrandingResponse - popinChild: PopInArgs | null; + popinChild: PopInArgs; loading: boolean; sidebar: SidebarStateProps; } @@ -64,7 +64,7 @@ export function initObject(): ReduxObject { terminal: initTerminalState(), providerBrandings: initProviderBranding(), branding: initBranding(), - popinChild: null, + popinChild: {el: undefined}, loading: false, sidebar: {favorites: [], theme: getThemeOrDefaultValue()} }; diff --git a/frontend-web/webclient/app/Editor/Editor.tsx b/frontend-web/webclient/app/Editor/Editor.tsx index 140864ec71..f84a0c1ad6 100644 --- a/frontend-web/webclient/app/Editor/Editor.tsx +++ b/frontend-web/webclient/app/Editor/Editor.tsx @@ -397,7 +397,7 @@ export const Editor: React.FunctionComponent<{ customContent?: React.ReactNode; showCustomContent?: boolean; onOpenFile?: (path: string, content: string | Uint8Array) => void; - operations?: (file: VirtualFile) => Operation[]; + operations?: (file?: VirtualFile) => Operation[]; help?: React.ReactNode; fileHeaderOperations?: React.ReactNode; renamingFile?: string; @@ -501,10 +501,10 @@ export const Editor: React.FunctionComponent<{ if (failedUpload) { sendFailureNotification(failedUpload.error ?? "Upload for file " + fileName(failedUpload.name) + " failed."); } - window.removeEventListener(FileWriteFailure, onFileWriteFailure) + window.removeEventListener(FileWriteFailure, {handleEvent: onFileWriteFailure}) } - window.addEventListener(FileWriteFailure, onFileWriteFailure) + window.addEventListener(FileWriteFailure, {handleEvent: onFileWriteFailure}) } }, confirmText: "Save changes", @@ -519,7 +519,7 @@ export const Editor: React.FunctionComponent<{ const {showReleaseNoteIcon, onShowReleaseNotesShown} = useShowReleaseNoteIcon(); - const [operations, setOperations] = useState[]>([]); + const [operations, setOperations] = useState[]>([]); const anyTabOpen = tabs.open.length > 0; const isSettingsOpen = state.currentPath === SETTINGS_PATH && anyTabOpen; const isReleaseNotesOpen = state.currentPath === RELEASE_NOTES_PATH && anyTabOpen; @@ -1298,7 +1298,7 @@ function tabOperations( tabs: {open: string[], closed: string[]}, dirtyFiles: Set, currentPath: string, -): Operation[] { +): Operation[] { const anyTabsOpen = tabs.open.length > 0; const anyTabsClosed = tabs.closed.length > 0; if (!tabPath) { diff --git a/frontend-web/webclient/app/ErrorBoundary/ErrorBoundary.tsx b/frontend-web/webclient/app/ErrorBoundary/ErrorBoundary.tsx index 8614b7f370..728a76cda2 100644 --- a/frontend-web/webclient/app/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend-web/webclient/app/ErrorBoundary/ErrorBoundary.tsx @@ -1,7 +1,7 @@ import {Client} from "@/Authentication/HttpClientInstance"; import {MainContainer} from "@/ui-components/MainContainer"; import * as React from "react"; -import {PRODUCT_NAME} from "@/../site.config.json" with {type: "json"}; +import {PRODUCT_NAME} from "@/../site.config.json"; import {Box, Button, Flex, TextArea} from "@/ui-components"; import {errorMessageOrDefault} from "@/UtilityFunctions"; import {sendFailureNotification} from "@/Notifications"; diff --git a/frontend-web/webclient/app/Files/DriveBrowse.tsx b/frontend-web/webclient/app/Files/DriveBrowse.tsx index 54666d02e3..cd9737e25a 100644 --- a/frontend-web/webclient/app/Files/DriveBrowse.tsx +++ b/frontend-web/webclient/app/Files/DriveBrowse.tsx @@ -14,7 +14,7 @@ import { import {useDispatch} from "react-redux"; import MainContainer from "@/ui-components/MainContainer"; import {callAPI, noopCall} from "@/Authentication/DataHook"; -import {api as FileCollectionsApi, FileCollection, FileCollectionSupport} from "@/UCloud/FileCollectionsApi"; +import {api as FileCollectionsApi, FileCollection, FileCollectionSupport, FileCollectionSpecification} from "@/UCloud/FileCollectionsApi"; import {AsyncCache} from "@/Utilities/AsyncCache"; import {FindByStringId, PageV2} from "@/UCloud"; import {dateToString} from "@/Utilities/DateUtilities"; @@ -26,7 +26,7 @@ import { retrieveSupportV2, SupportByProviderV2, supportV2ProductMatch } from "@/UCloud/ResourceApi"; -import {ProductV2, ProductV2Storage} from "@/Accounting"; +import {ProductStorage, ProductV2, ProductV2Storage} from "@/Accounting"; import {bulkRequestOf} from "@/UtilityFunctions"; import {usePage} from "@/Navigation/Redux"; import AppRoutes from "@/Routes"; @@ -207,7 +207,7 @@ const DriveBrowse: React.FunctionComponent<{ browser.on("fetchOperationsCallback", () => { const cachedSupport = supportByProvider.retrieveFromCacheOnly(Client.projectId ?? ""); const support = cachedSupport ?? {productsByProvider: {}}; - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { supportByProvider: support, dispatch, isWorkspaceAdmin: isWorkspaceAdmin.current, @@ -308,7 +308,7 @@ const DriveBrowse: React.FunctionComponent<{ browser.renderRows(); dialogStore.success(); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to create new drive. " + extractErrorMessage(e)); browser.refresh(); return; diff --git a/frontend-web/webclient/app/Files/FileBrowse.tsx b/frontend-web/webclient/app/Files/FileBrowse.tsx index 3c09b8f4f6..e439130274 100644 --- a/frontend-web/webclient/app/Files/FileBrowse.tsx +++ b/frontend-web/webclient/app/Files/FileBrowse.tsx @@ -56,7 +56,7 @@ import MetadataNamespaceApi, {FileMetadataTemplateNamespace} from "@/UCloud/Meta import {bulkRequestOf} from "@/UtilityFunctions"; import metadataDocumentApi, {FileMetadataDocument, FileMetadataDocumentOrDeleted, FileMetadataHistory} from "@/UCloud/MetadataDocumentApi"; -import {ResourceBrowseCallbacks, ResourceOwner, ResourcePermissions, SupportByProvider} from "@/UCloud/ResourceApi"; +import {ResourceBrowseCallbacks, ResourceOwner, ResourcePermissions, ResourceSpecification, SupportByProvider} from "@/UCloud/ResourceApi"; import {Client, WSFactory} from "@/Authentication/HttpClientInstance"; import ProductReference = accounting.ProductReference; import {Operation} from "@/ui-components/Operation"; @@ -75,6 +75,7 @@ import {SidebarTabId} from "@/ui-components/SidebarComponents"; import {HTMLTooltip} from "@/ui-components/Tooltip"; import SharesApi, {OutgoingShareGroup} from "@/UCloud/SharesApi"; import {sendFailureNotification, sendSuccessNotification} from "@/Notifications"; +import {ProductStorage} from "@/Accounting"; export enum SensitivityLevel { "INHERIT" = "Inherit", @@ -681,8 +682,9 @@ function FileBrowse({ const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks & ExtraFileCallbacks; const enabledOperations = FilesApi.retrieveOperations().filter(op => op.enabled(selected, callbacks, selected)); if (opts?.additionalOperations) { - opts.additionalOperations.forEach(op => { - if (op.enabled(selected, callbacks, selected)) enabledOperations.push(op); + (opts.additionalOperations).forEach(op => { + // FIXME(Jonas): Casting here is not ideal + if (op.enabled(selected, callbacks, selected)) enabledOperations.push(op as unknown as Operation>); }) } return groupOperations(enabledOperations); @@ -1633,7 +1635,7 @@ function folderNote( } // Note(Jonas): Temporary as there should be a better solution, not because the element is temporary -function temporaryDriveDropdownFunction(browser: ResourceBrowser, posX: number, posY: number): void { +function temporaryDriveDropdownFunction(browser: ResourceBrowser, posX: number, posY: number): void { const filteredCollections = collectionCacheForCompletion.retrieveFromCacheOnly("") ?? []; function generateElements(filter?: string): HTMLLIElement[] { diff --git a/frontend-web/webclient/app/Files/FileTree.tsx b/frontend-web/webclient/app/Files/FileTree.tsx index 658861ffa9..381f37d84e 100644 --- a/frontend-web/webclient/app/Files/FileTree.tsx +++ b/frontend-web/webclient/app/Files/FileTree.tsx @@ -28,7 +28,7 @@ interface FileTreeProps { root: EditorSidebarNode; initialFolder: string; initialFilePath?: string; - operations?: (file?: VirtualFile) => Operation[]; + operations?: (file?: VirtualFile) => Operation[]; width?: string; canResize?: boolean; fileHeaderOperations?: React.ReactNode; @@ -40,7 +40,7 @@ export function FileTree({tree, onTreeAction, onNodeActivated, root, ...props}: const width = props.width ?? "250px"; const resizeSetting = props.canResize ? "horizontal" : "none"; - const [operations, setOperations] = React.useState[]>([]); + const [operations, setOperations] = React.useState[]>([]); const style = { "--tree-width": width, @@ -93,7 +93,6 @@ export function FileTree({tree, onTreeAction, onNodeActivated, root, ...props}: openFnRef={openOperations} selected={[]} extra={null} - row={42} hidden location={"IN_ROW"} /> diff --git a/frontend-web/webclient/app/Files/HTML5FileSelector.ts b/frontend-web/webclient/app/Files/HTML5FileSelector.ts index 24423dd8ce..4af601f559 100644 --- a/frontend-web/webclient/app/Files/HTML5FileSelector.ts +++ b/frontend-web/webclient/app/Files/HTML5FileSelector.ts @@ -145,11 +145,11 @@ interface DropEventFolder { fileFetcher: () => Promise; } -export async function filesFromDropOrSelectEvent(event): Promise { +export async function filesFromDropOrSelectEvent(event: React.DragEvent): Promise { const dataTransfer = event.dataTransfer; if (!dataTransfer) { const files: PackagedFile[] = []; - const inputFieldFileList = event.target && event.target.files; + const inputFieldFileList: File[] | undefined = event.target && event.target["files"]; const fileList = inputFieldFileList || []; for (let i = 0; i < fileList.length; i++) { @@ -157,18 +157,22 @@ export async function filesFromDropOrSelectEvent(event): Promise { } return files.map(file => { - const thisFile = new Promise((resolve) => resolve(file)); + const thisFile = new Promise((resolve) => resolve(file)); return {type: "single", file: thisFile} }); } const entries: FileSystemEntry[] = []; - [].slice.call(dataTransfer.items).forEach((listItem) => { - if (typeof listItem.webkitGetAsEntry === 'function') { - const entry: FileSystemEntry = listItem.webkitGetAsEntry(); - entries.push(entry); + [...dataTransfer.items].forEach((listItem) => { + if (typeof listItem['webkitGetAsEntry'] === 'function') { + /* TODO(Jonas): The FileSystemEntry is now defined by TS-definitions, but they don't match with the file-local version */ + const entry: FileSystemEntry | null = listItem.webkitGetAsEntry() as unknown as FileSystemEntry; + if (entry) { + entries.push(entry); + } } else { - const theFile: File = listItem.getAsFile(); + const theFile: File | null = listItem.getAsFile(); + if (!theFile) return; const entry: FileSystemEntry = { filesystem: 1, diff --git a/frontend-web/webclient/app/Files/Shares.tsx b/frontend-web/webclient/app/Files/Shares.tsx index fa1ad78928..e71b49b589 100644 --- a/frontend-web/webclient/app/Files/Shares.tsx +++ b/frontend-web/webclient/app/Files/Shares.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import {useEffect, useRef, useState} from "react"; -import SharesApi, {Share, ShareLink, shareLinksApi, ShareState} from "@/UCloud/SharesApi"; +import SharesApi, {Share, ShareLink, shareLinksApi, ShareSpecification, ShareState} from "@/UCloud/SharesApi"; import {NavigateFunction, useNavigate} from "react-router-dom"; import {buildQueryString} from "@/Utilities/URIUtilities"; import * as Heading from "@/ui-components/Heading"; @@ -54,6 +54,7 @@ import {TruncateClass} from "@/ui-components/Truncate"; import {defaultAvatar} from "@/AvataaarLib"; import {SvgCache} from "@/Utilities/SvgCache"; import {sendInformationNotification} from "@/Notifications"; +import {Product} from "@/Accounting"; export const sharesLinksInfo: LinkInfo[] = [ {text: "Shared with me", to: AppRoutes.shares.sharedWithMe(), icon: "share", tab: SidebarTabId.FILES, defaultHidden: true}, @@ -631,7 +632,7 @@ export function IngoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts & browser.on("pathToEntry", s => s.id); browser.on("fetchOperationsCallback", () => { const support = {productsByProvider: {}}; - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { api: SharesApi, navigate: to => navigate(to), commandLoading: false, diff --git a/frontend-web/webclient/app/Files/SharesOutgoing.tsx b/frontend-web/webclient/app/Files/SharesOutgoing.tsx index 5983ba7381..afecd125ab 100644 --- a/frontend-web/webclient/app/Files/SharesOutgoing.tsx +++ b/frontend-web/webclient/app/Files/SharesOutgoing.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import {usePage} from "@/Navigation/Redux"; -import SharesApi, {OutgoingShareGroup, OutgoingShareGroupPreview, Share, ShareState, isViewingShareGroupPreview} from "@/UCloud/SharesApi"; +import SharesApi, {OutgoingShareGroup, OutgoingShareGroupPreview, Share, ShareSpecification, ShareState, isViewingShareGroupPreview} from "@/UCloud/SharesApi"; import MainContainer from "@/ui-components/MainContainer"; import {prettyFilePath} from "@/Files/FilePath"; import {RadioTile, RadioTilesContainer} from "@/ui-components"; @@ -27,6 +27,7 @@ import {useProjectId} from "@/Project/Api"; import {FlexClass} from "@/ui-components/Flex"; import {SidebarTabId} from "@/ui-components/SidebarComponents"; import {sendFailureNotification} from "@/Notifications"; +import {Product} from "@/Accounting"; enum ShareValidateState { NOT_VALIDATED, @@ -168,13 +169,13 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts (it as any).id === dummyId); shouldRemoveFakeDirectory = false; insertFakeEntry(dummyId); - const idx = browser.findVirtualRowIndex((it: OutgoingShareGroupPreview) => it.shareId === dummyId); + const idx = browser.findVirtualRowIndex((it) => (it as OutgoingShareGroupPreview).shareId === dummyId); if (idx !== null) browser.ensureRowIsVisible(idx, true); browser.showRenameField( - (it: OutgoingShareGroupPreview) => it.shareId === dummyId, + (it) => (it as OutgoingShareGroupPreview).shareId === dummyId, () => { - const idx = browser.findVirtualRowIndex((it: OutgoingShareGroupPreview) => it.shareId === dummyId); + const idx = browser.findVirtualRowIndex((it) => (it as OutgoingShareGroupPreview).shareId === dummyId); if (idx !== null) { browser.ensureRowIsVisible(idx, true, true); browser.select(idx, SelectionMode.SINGLE); @@ -183,7 +184,7 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts it.shareId === dummyId); + browser.removeEntryFromCurrentPage(it => (it as OutgoingShareGroupPreview).shareId === dummyId); const sharedWith = browser.renameValue; if (!sharedWith) return; @@ -204,7 +205,7 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts { - if (shouldRemoveFakeDirectory) browser.removeEntryFromCurrentPage((it: OutgoingShareGroupPreview) => it.shareId === dummyId); + if (shouldRemoveFakeDirectory) browser.removeEntryFromCurrentPage(it => (it as OutgoingShareGroupPreview).shareId === dummyId); }, "" ); @@ -429,7 +430,7 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts it.sourceFilePath !== share.sourceFilePath)), + arrayToPage(oldPage.filter(it => (it as OutgoingShareGroup).sourceFilePath !== share.sourceFilePath)), "/", true ); @@ -555,7 +556,7 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts { const support = {productsByProvider: {}}; - const callbacks: ResourceBrowseCallbacks = { + const callbacks: ResourceBrowseCallbacks = { api: SharesApi, navigate: to => navigate(to), commandLoading: false, @@ -573,9 +574,9 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts { const entries = browser.findSelectedEntries(); - const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; + const callbacks = browser.dispatchMessage("fetchOperationsCallback", fn => fn()) as ResourceBrowseCallbacks; - const operations: Operation>[] = [{ + const operations: Operation>[] = [{ text: "Delete", confirm: true, icon: "trash", @@ -639,8 +640,8 @@ export function OutgoingSharesBrowse({opts}: {opts?: ResourceBrowserOpts { clearUploads: b => uploadStore.clearUploads(b, setPausedFilesInFolder), }), [startUploads]); - const onSelectedFile = useCallback(async (e: {stopPropagation(): void; preventDefault(): void}, isResuming = false) => { + const onSelectedFile = useCallback(async function (e: T, isResuming: boolean = false) { e.preventDefault(); e.stopPropagation(); const allUploads: Upload[] = uploads; - const events = await filesFromDropOrSelectEvent(e); + /* TODO(Jonas): Typesafety should be improved */ + const events = await filesFromDropOrSelectEvent(e as unknown as React.DragEvent); for (const u of events) { switch (u.type) { case "single": { @@ -730,7 +731,8 @@ const Uploader: React.FunctionComponent = () => { startUploads(allUploads, setLookForNewUploads); }, [uploads]); - const stopGapMethodForUploadingFilesFromTheEditor = React.useCallback((e: CustomEvent) => { + const stopGapMethodForUploadingFilesFromTheEditor = React.useCallback((_e: unknown) => { + const e = _e as CustomEvent; e.stopImmediatePropagation(); e.stopPropagation(); e.preventDefault(); diff --git a/frontend-web/webclient/app/Grants/Editor.tsx b/frontend-web/webclient/app/Grants/Editor.tsx index 941d49da41..06f302a167 100644 --- a/frontend-web/webclient/app/Grants/Editor.tsx +++ b/frontend-web/webclient/app/Grants/Editor.tsx @@ -1540,7 +1540,7 @@ export function Editor(): React.ReactNode { dispatchEvent({type: "Unlock"}); }, [dispatchEvent]); - const onWithdraw = useCallback(() => { + const onWithdraw = useCallback(async () => { if (!state.stateDuringEdit) return; dispatchEvent({type: "Withdraw", id: state.stateDuringEdit.id}); }, [dispatchEvent, state.stateDuringEdit?.id]); @@ -1600,7 +1600,7 @@ export function Editor(): React.ReactNode { } }, [state, dispatchEvent, missingUserInfo]); - const onDiscard = useCallback(() => { + const onDiscard = useCallback(async () => { const id = state.stateDuringEdit?.id; if (!id) return; dispatchEvent({type: "Init", grantId: id}); @@ -2718,11 +2718,11 @@ const GrantGiver: React.FunctionComponent<{ const isAdmin = props.adminOfProjects.some(it => it.id === props.projectId) - const onApprove = useCallback(() => { + const onApprove = useCallback(async () => { props.onStateChange(props.projectId, true); }, [props.onStateChange, props.projectId]); - const onReject = useCallback(() => { + const onReject = useCallback(async () => { props.onStateChange(props.projectId, false); }, [props.onStateChange, props.projectId]); diff --git a/frontend-web/webclient/app/Login/Login.tsx b/frontend-web/webclient/app/Login/Login.tsx index 93d4bea6a0..b781caff16 100644 --- a/frontend-web/webclient/app/Login/Login.tsx +++ b/frontend-web/webclient/app/Login/Login.tsx @@ -38,8 +38,8 @@ export const LOGIN_REDIRECT_KEY = "redirect_on_login"; const inDevEnvironment = DEVELOPMENT_ENV; const enabledWayf = true; -async function fetchBranding (){ - return await fetch(Client.computeURL("/api/branding", "/retrieve"), +async function fetchBranding() { + return await fetch(Client.computeURL("/api/branding", "/retrieve"), { method: "GET", headers: { @@ -68,8 +68,8 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { const [isGeneric, setIsGeneric] = useState(false); const [textColor, setTextColor] = useState("#fff"); const [showingWayf, setShowingWayf] = useState(false); - const [branding, setBranding] = useState( - {deploymentName: "", loginPage: {primaryLogoUrl: "", secondaryLogoUrls: [], type: BrandingLoginPageType.GENERIC} + const [branding, setBranding] = useState({ + deploymentName: "", loginPage: {primaryLogoUrl: "", secondaryLogoUrls: [], type: BrandingLoginPageType.GENERIC} }); const promises = usePromiseKeeper(); @@ -149,7 +149,7 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { } handleAuthState(await response.json()); - } catch (e) { + } catch (e: any) { sendFailureNotification( errorMessageOrDefault({ request: e, @@ -207,7 +207,7 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { sendSuccessNotification("Your password was changed successfully"); navigate(AppRoutes.login.login()); - } catch (err) { + } catch (err: any) { setLoading(false); sendFailureNotification(err.statusText); @@ -247,7 +247,7 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { if (!response.ok) throw response; const result = await response.json(); handleCompleteLogin(result); - } catch (e) { + } catch (e: any) { setLoading(false); sendFailureNotification( errorMessageOrDefault({ @@ -286,7 +286,7 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { return ( - + {enabledWayf && !challengeId && !isPasswordReset && showingWayf ? (<> @@ -584,7 +584,7 @@ type IsGenericProps = { }; type BackgroundImageProps = React.PropsWithChildren<{ - isGeneric: boolean; + isGeneric: boolean; }>; function BackgroundImage({isGeneric, children}: BackgroundImageProps) { @@ -657,7 +657,7 @@ const IdpList: React.FunctionComponent = ({isGeneric}) => { } }; -function LoginHeader ({branding}: React.PropsWithChildren): React.ReactNode { +function LoginHeader({branding}: React.PropsWithChildren): React.ReactNode { const textColor = getTextColor(branding); if (isUsingGenericLoginPage(branding)) { diff --git a/frontend-web/webclient/app/Navigation/Redux/index.tsx b/frontend-web/webclient/app/Navigation/Redux/index.tsx index 0a7e834879..ff11f502a9 100644 --- a/frontend-web/webclient/app/Navigation/Redux/index.tsx +++ b/frontend-web/webclient/app/Navigation/Redux/index.tsx @@ -1,42 +1,9 @@ +import {SidebarTabId} from "@/ui-components/SidebarComponents"; import {PRODUCT_NAME} from "../../../site.config.json"; -import {SetLoadingAction} from "@/Types"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import {useDispatch} from "react-redux"; import {useEffect} from "react"; -import {initStatus, StatusReduxObject} from "@/DefaultObjects"; -import {SidebarTabId} from "@/ui-components/SidebarComponents"; -import {PayloadAction} from "@reduxjs/toolkit"; - -export type Index = UpdatePageTitleAction | SetLoading | SetActivePage; - -export type UpdatePageTitleAction = PayloadAction<{title: string}, typeof UPDATE_PAGE_TITLE>; -/** - * Sets the title of the window. Stores in the redux store as well - * @param {string} title the title to be set - */ -export const updatePageTitle = (title: string): UpdatePageTitleAction => ({ - type: UPDATE_PAGE_TITLE, - payload: {title} -}); - -type SetLoading = SetLoadingAction; -export function setLoading(loading: boolean): SetLoading { - return ({ - type: SET_STATUS_LOADING, - payload: {loading} - }); -} - -type SetActivePage = PayloadAction<{tab: SidebarTabId}, typeof SET_ACTIVE_PAGE> -function setActivePage(tab: SidebarTabId): SetActivePage { - return { - type: SET_ACTIVE_PAGE, - payload: {tab} - } -} - -export interface SetStatusLoading { - setLoading: (loading: boolean) => void; -} +import {initStatus} from "@/DefaultObjects"; export function usePage(title: string, tab: SidebarTabId): void { const dispatch = useDispatch(); @@ -53,25 +20,27 @@ export function usePage(title: string, tab: SidebarTabId): void { export function useLoading(loading: boolean): void { const dispatch = useDispatch(); useEffect(() => { - dispatch(setLoading(loading)); + dispatch(setStatusLoading(loading)); }, [loading]); } -export const UPDATE_PAGE_TITLE = "UPDATE_PAGE_TITLE"; -export const SET_STATUS_LOADING = "SET_STATUS_LOADING"; -export const SET_ACTIVE_PAGE = "SET_ACTIVE_PAGE"; - -export const statusReducer = (state: StatusReduxObject = initStatus(), action: Index): StatusReduxObject => { - switch (action.type) { - case UPDATE_PAGE_TITLE: - document.title = `${PRODUCT_NAME} | ${action.payload.title}`; - return {...state, ...action.payload}; - case SET_STATUS_LOADING: - case SET_ACTIVE_PAGE: - return {...state, ...action.payload}; - default: { - return state; +export const statusSlice = createSlice({ + name: "status", + initialState: initStatus(), + reducers: { + updatePageTitle(state, action: PayloadAction) { + document.title = `${PRODUCT_NAME} | ${action.payload}`; + state.title = action.payload; + }, + setStatusLoading(state, action: PayloadAction) { + state.loading = action.payload; + }, + setActivePage(state, action: PayloadAction) { + state.tab = action.payload; } - } -}; + }, +}) + +export const {updatePageTitle, setStatusLoading, setActivePage} = statusSlice.actions; +export const statusReducer = statusSlice.reducer; diff --git a/frontend-web/webclient/app/Project/ProjectSwitcher.tsx b/frontend-web/webclient/app/Project/ProjectSwitcher.tsx index c0e4c71f75..4b02336ff1 100644 --- a/frontend-web/webclient/app/Project/ProjectSwitcher.tsx +++ b/frontend-web/webclient/app/Project/ProjectSwitcher.tsx @@ -3,7 +3,7 @@ import {useDispatch} from "react-redux"; import {bulkRequestOf, threadDeferLike, displayErrorMessageOrDefault, errorMessageOrDefault, stopPropagationAndPreventDefault} from "@/UtilityFunctions"; import {useEffect} from "react"; import {dispatchSetProjectAction, emitProjects, getStoredProject} from "@/Project/ReduxState"; -import {Flex, Truncate, Text, Icon, Input, Relative, Box, Error, Tooltip, Label} from "@/ui-components"; +import {Flex, Truncate, Icon, Input, Relative, Box, Error, Label} from "@/ui-components"; import ClickableDropdown from "@/ui-components/ClickableDropdown"; import {callAPI, useCloudCommand} from "@/Authentication/DataHook"; import {NavigateFunction, useNavigate} from "react-router-dom"; diff --git a/frontend-web/webclient/app/Project/ReduxState.ts b/frontend-web/webclient/app/Project/ReduxState.ts index 7283ee3d05..f655f07dde 100644 --- a/frontend-web/webclient/app/Project/ReduxState.ts +++ b/frontend-web/webclient/app/Project/ReduxState.ts @@ -1,31 +1,28 @@ -import {PayloadAction} from "@reduxjs/toolkit"; -import {Dispatch} from "redux"; +import {createSlice, Dispatch, PayloadAction} from "@reduxjs/toolkit"; export interface State { project?: string; } -export const initialState = {project: getStoredProject() ?? undefined}; - -type SetProjectAction = PayloadAction<{project?: string}, "SET_PROJECT"> +export const initialState: State = {project: getStoredProject() ?? undefined}; export function dispatchSetProjectAction(dispatch: Dispatch, project?: string): void { - dispatch({payload: {project}, type: "SET_PROJECT"}); + dispatch(setProject(project)); } -type ProjectAction = SetProjectAction; - -export const reducer = (state: State = initialState, action: ProjectAction): State => { - switch (action.type) { - case "SET_PROJECT": { - setStoredProject(action.payload.project ?? null); - return {...state, project: action.payload.project}; +const projectSlice = createSlice({ + name: "project", + initialState, + reducers: { + setProject(state, action: PayloadAction) { + setStoredProject(action.payload ?? null); + state.project = action.payload; } - - default: - return state; } -}; +}); + +export const {setProject} = projectSlice.actions; +export const reducer = projectSlice.reducer; export function getStoredProject(): string | null { return window.localStorage.getItem("project") ?? null; diff --git a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx index ef0337c187..6c162e6af1 100644 --- a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx +++ b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx @@ -1,13 +1,13 @@ import * as React from "react"; import {useCloudAPI} from "@/Authentication/DataHook"; -import {providerBrandingApi, ProviderBrandingResponse } from "@/UCloud/ProviderBrandingApi"; +import {providerBrandingApi, ProviderBrandingResponse} from "@/UCloud/ProviderBrandingApi"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import {useDispatch} from "react-redux"; -import {PayloadAction} from "@reduxjs/toolkit"; export const AutomaticProviderBranding: React.FunctionComponent = () => { const [providerBrandings, fetchBranding] = useCloudAPI( providerBrandingApi.browse(), - { providers: {} } + {providers: {}} ); React.useEffect(() => { @@ -22,16 +22,12 @@ export const AutomaticProviderBranding: React.FunctionComponent = () => { const dispatch = useDispatch(); React.useEffect(() => { - dispatch({type: ADD_PROVIDER_BRANDING, payload: providerBrandings.data}); + dispatch(addProviderBranding(providerBrandings.data)); }, [providerBrandings.data]); return null; }; -const ADD_PROVIDER_BRANDING = "ADD_PROVIDER_BRANDING"; -type SetProviderBranding = PayloadAction - -type ProviderBrandingAction = SetProviderBranding; export function initProviderBranding(): ProviderBrandingResponse { return { @@ -39,12 +35,15 @@ export function initProviderBranding(): ProviderBrandingResponse { } } -export function providerBrandingReducer(state: ProviderBrandingResponse = initProviderBranding(), action: ProviderBrandingAction): ProviderBrandingResponse { - switch (action.type) { - case ADD_PROVIDER_BRANDING: { - return action.payload; - } - default: - return state; +const providerBrandingSlice = createSlice({ + name: "providerBranding", + initialState: initProviderBranding(), + reducers: { + addProviderBranding(state, action: PayloadAction) { + state = action.payload; + } } -} \ No newline at end of file +}); + +const {addProviderBranding} = providerBrandingSlice.actions; +export const providerBrandingReducer = providerBrandingSlice.reducer; \ No newline at end of file diff --git a/frontend-web/webclient/app/Providers/Connection.tsx b/frontend-web/webclient/app/Providers/Connection.tsx index 2f9c584d10..e79384c15f 100644 --- a/frontend-web/webclient/app/Providers/Connection.tsx +++ b/frontend-web/webclient/app/Providers/Connection.tsx @@ -25,7 +25,7 @@ const Connection: React.FunctionComponent = () => { ) ); navigate("/"); - } catch (e) { + } catch (e: any) { setErrorMessage(extractErrorMessage(e)); } })(); diff --git a/frontend-web/webclient/app/Resource/PermissionEditor.tsx b/frontend-web/webclient/app/Resource/PermissionEditor.tsx index c1ad3d052a..9d64ac6172 100644 --- a/frontend-web/webclient/app/Resource/PermissionEditor.tsx +++ b/frontend-web/webclient/app/Resource/PermissionEditor.tsx @@ -12,23 +12,25 @@ import { Resource, ResourceAclEntry, ResourceApi, + ResourceSpecification, } from "@/UCloud/ResourceApi"; import {useProjectId} from "@/Project/Api"; import {useProject} from "@/Project/cache"; import Spinner from "@/LoadingIcon/LoadingIcon"; import {classConcat} from "@/Unstyled"; import {Toggle} from "@/ui-components/Toggle"; +import {Product} from "@/Accounting"; -interface ResourcePermissionEditorProps { +interface ResourcePermissionEditorProps { reload: () => void; - entity: T; - api: ResourceApi; + entity: Res; + api: ResourceApi; showMissingPermissionHelp?: boolean; noPermissionsWarning?: string; } -export function ResourcePermissionEditor( - props: ResourcePermissionEditorProps +export function ResourcePermissionEditor( + props: ResourcePermissionEditorProps ): React.ReactNode { const {entity, reload, api} = props; const projectId = useProjectId(); diff --git a/frontend-web/webclient/app/Resource/Properties.tsx b/frontend-web/webclient/app/Resource/Properties.tsx index b049adb036..73397cab14 100644 --- a/frontend-web/webclient/app/Resource/Properties.tsx +++ b/frontend-web/webclient/app/Resource/Properties.tsx @@ -7,6 +7,7 @@ import { ResourceAclEntry, ResourceApi, ResourceBrowseCallbacks, + ResourceSpecification, SupportByProvider, UCLOUD_CORE } from "@/UCloud/ResourceApi"; @@ -34,6 +35,7 @@ import {SidebarTabId} from "@/ui-components/SidebarComponents"; import TabbedCard, {TabbedCardTab} from "@/ui-components/TabbedCard"; import {EmbeddedSettings} from "@/ui-components/ResourceBrowser"; import {Client} from "@/Authentication/HttpClientInstance"; +import {Product} from "@/Accounting"; const enterAnimation = makeKeyframe("enter-animation", ` from { @@ -166,8 +168,8 @@ const ContentWrapper = injectStyleSimple("content-wrapper", ` grid-gap: 16px; `); -interface PropertiesProps { - api: ResourceApi; +interface PropertiesProps { + api: ResourceApi; embedded?: EmbeddedSettings; classname?: string; @@ -188,8 +190,8 @@ interface PropertiesProps { flagsForRetrieve?: Record; } -export function ResourceProperties( - props: PropsWithChildren> +export function ResourceProperties( + props: PropsWithChildren> ): ReactElement | null { const {api} = props; @@ -252,7 +254,7 @@ export function ResourceProperties( return result; }, [resource]); - const callbacks: ResourceBrowseCallbacks = useMemo(() => ({ + const callbacks: ResourceBrowseCallbacks = useMemo(() => ({ api, isCreating: false, navigate, @@ -378,7 +380,7 @@ function canEditPermission(support: ProductSupport | undefined, namespace: strin } // TODO(Jonas): Find a less dramatic name -function PredicatedPermissionsTable(props: {show?: boolean; api: ResourceApi; res: Resource | null}): React.ReactNode { +function PredicatedPermissionsTable(props: {show?: boolean; api: ResourceApi; res: Resource | null}): React.ReactNode { const [acl, setAcl] = React.useState(props.res?.permissions.others ?? []); React.useEffect(() => { diff --git a/frontend-web/webclient/app/Resource/Router.tsx b/frontend-web/webclient/app/Resource/Router.tsx index d0e746908b..b8fd9b7a8f 100644 --- a/frontend-web/webclient/app/Resource/Router.tsx +++ b/frontend-web/webclient/app/Resource/Router.tsx @@ -1,15 +1,16 @@ import * as React from "react"; -import {Resource, ResourceApi} from "@/UCloud/ResourceApi"; +import {Resource, ResourceApi, ResourceSpecification} from "@/UCloud/ResourceApi"; import {PropsWithChildren, ReactElement} from "react"; import {Route, Routes} from "react-router-dom"; +import {Product} from "@/Accounting"; -interface RouterProps { - api: ResourceApi; +interface RouterProps { + api: ResourceApi; Browser: React.FunctionComponent; Create?: React.FunctionComponent; } -export function ResourceRouter(props: PropsWithChildren>): ReactElement | null { +export function ResourceRouter(props: PropsWithChildren>): ReactElement | null { const Properties = props.api.Properties; return } /> diff --git a/frontend-web/webclient/app/ServiceLicenseAgreement/index.tsx b/frontend-web/webclient/app/ServiceLicenseAgreement/index.tsx index 7679efd40c..48518ef1b6 100644 --- a/frontend-web/webclient/app/ServiceLicenseAgreement/index.tsx +++ b/frontend-web/webclient/app/ServiceLicenseAgreement/index.tsx @@ -47,8 +47,8 @@ const ServiceLicenseAgreement: React.FunctionComponent = () => { await invokeCommand(acceptSla(sla.data.version)); await Client.invalidateAccessToken(); navigate("/"); - } catch (res) { - const response = res.response; + } catch (err: any) { + const response = err.response; const why: string = response?.why ?? "Error while attempting to accept agreement"; sendFailureNotification(why); } diff --git a/frontend-web/webclient/app/Stacks/StackView.tsx b/frontend-web/webclient/app/Stacks/StackView.tsx index 96efca1a37..037b1dfe1c 100644 --- a/frontend-web/webclient/app/Stacks/StackView.tsx +++ b/frontend-web/webclient/app/Stacks/StackView.tsx @@ -38,11 +38,11 @@ type MachinesLabelFilter = { }; export default function StackView(): React.ReactNode { - const {id} = useParams<{id: string}>(); - const navigate = useNavigate(); - const [stackState, fetchStack] = useCloudAPI({noop: true}, null); - const [commandLoading, invokeCommand] = useCloudCommand(); - const [ucxAuthenticated, setUcxAuthenticated] = React.useState(false); + const {id} = useParams<{id: string}>(); + const navigate = useNavigate(); + const [stackState, fetchStack] = useCloudAPI({noop: true}, null); + const [commandLoading, invokeCommand] = useCloudCommand(); + const [ucxAuthenticated, setUcxAuthenticated] = React.useState(false); usePage("Stack", SidebarTabId.RUNS); @@ -55,99 +55,99 @@ export default function StackView(): React.ReactNode { refreshStack(); }, [refreshStack]); - const stack = stackState.data; - const status = stack?.status; - const jobs = status?.jobs ?? []; - const uiMode = status?.ucxUiMode ?? "None"; - const ucxConnectJobId = status?.ucxConnectJobId ?? null; - const ucxConnectJob = React.useMemo(() => { - if (!ucxConnectJobId) return null; - return jobs.find(job => job.id === ucxConnectJobId) ?? null; - }, [jobs, ucxConnectJobId]); - const ucxTargetRunning = ucxConnectJob?.status.state === "RUNNING"; - const shouldAttemptUcxConnection = uiMode === "Replacement" && !!ucxConnectJobId && ucxTargetRunning; - - const ucxConnectJobUrl = React.useMemo(() => { - return Client.computeURL("/api", "/hpc/apps/ucx/connectJob") - .replace("http://", "ws://") - .replace("https://", "wss://"); - }, []); - - const ucxRpcHandlers = React.useMemo>(() => { - const connectJobSpecification = ucxConnectJob?.specification as (Job["specification"] & {labels?: Record}) | undefined; - const stackStateFolder = connectJobSpecification?.labels?.["ucloud.dk/stack-state-folder"]; - const stackPathToFile = (fileName: string) => { - const trimmedFileName = fileName.trim(); - if (!trimmedFileName) { - throw new Error("Missing file name"); - } - - const trimmedBase = (stackStateFolder ?? "").trim(); - if (!trimmedBase) { - throw new Error("Stack state folder not found on connected job"); - } - - return `${trimmedBase.replace(/\/+$/, "")}/${trimmedFileName.replace(/^\/+/, "")}`; - }; - - return { - uiSendMessage: raw => { - const payload = raw as {message: string; success: boolean}; - if (!payload.success) { - sendFailureNotification(payload.message); - } - }, - stackRefresh: async () => { - refreshStack(); - }, - stackOpen: raw => { - const payload = raw as {id: string}; - navigate(`${AppRoutes.prefix}${AppRoutes.stacks.view(payload.id)}`); - }, - stackCopyFile: async raw => { - try { - const payload = raw as {fileName?: string}; - const fileName = payload.fileName?.trim() ?? ""; - const path = stackPathToFile(fileName); - const blob = await downloadFileContent(path); - await copyToClipboard(await blob.text()); - sendSuccessNotification(`Copied ${fileName} to clipboard`); - } catch (err) { - sendFailureNotification(err instanceof Error ? err.message : "Failed to copy stack file content"); - } - }, - stackDownloadFile: async raw => { - try { - const payload = raw as {fileName?: string}; - const fileName = payload.fileName?.trim() ?? ""; - const path = stackPathToFile(fileName); - const blob = await downloadFileContent(path); - const link = document.createElement("a"); - const blobUrl = URL.createObjectURL(blob); - link.href = blobUrl; - link.download = fileName; - document.body.appendChild(link); - link.click(); - if (link.parentNode === document.body) { - document.body.removeChild(link); - } - URL.revokeObjectURL(blobUrl); - } catch (err) { - sendFailureNotification(err instanceof Error ? err.message : "Failed to download stack file"); - } - }, - }; - }, [navigate, refreshStack, ucxConnectJob]); - - React.useEffect(() => { - setUcxAuthenticated(false); - }, [ucxConnectJobId]); - - React.useEffect(() => { - if (!shouldAttemptUcxConnection) { - setUcxAuthenticated(false); - } - }, [shouldAttemptUcxConnection]); + const stack = stackState.data; + const status = stack?.status; + const jobs = status?.jobs ?? []; + const uiMode = status?.ucxUiMode ?? "None"; + const ucxConnectJobId = status?.ucxConnectJobId ?? null; + const ucxConnectJob = React.useMemo(() => { + if (!ucxConnectJobId) return null; + return jobs.find(job => job.id === ucxConnectJobId) ?? null; + }, [jobs, ucxConnectJobId]); + const ucxTargetRunning = ucxConnectJob?.status.state === "RUNNING"; + const shouldAttemptUcxConnection = uiMode === "Replacement" && !!ucxConnectJobId && ucxTargetRunning; + + const ucxConnectJobUrl = React.useMemo(() => { + return Client.computeURL("/api", "/hpc/apps/ucx/connectJob") + .replace("http://", "ws://") + .replace("https://", "wss://"); + }, []); + + const ucxRpcHandlers = React.useMemo>(() => { + const connectJobSpecification = ucxConnectJob?.specification as (Job["specification"] & {labels?: Record}) | undefined; + const stackStateFolder = connectJobSpecification?.labels?.["ucloud.dk/stack-state-folder"]; + const stackPathToFile = (fileName: string) => { + const trimmedFileName = fileName.trim(); + if (!trimmedFileName) { + throw new Error("Missing file name"); + } + + const trimmedBase = (stackStateFolder ?? "").trim(); + if (!trimmedBase) { + throw new Error("Stack state folder not found on connected job"); + } + + return `${trimmedBase.replace(/\/+$/, "")}/${trimmedFileName.replace(/^\/+/, "")}`; + }; + + return { + uiSendMessage: raw => { + const payload = raw as {message: string; success: boolean}; + if (!payload.success) { + sendFailureNotification(payload.message); + } + }, + stackRefresh: async () => { + refreshStack(); + }, + stackOpen: raw => { + const payload = raw as {id: string}; + navigate(`${AppRoutes.prefix}${AppRoutes.stacks.view(payload.id)}`); + }, + stackCopyFile: async raw => { + try { + const payload = raw as {fileName?: string}; + const fileName = payload.fileName?.trim() ?? ""; + const path = stackPathToFile(fileName); + const blob = await downloadFileContent(path); + await copyToClipboard(await blob.text()); + sendSuccessNotification(`Copied ${fileName} to clipboard`); + } catch (err) { + sendFailureNotification(err instanceof Error ? err.message : "Failed to copy stack file content"); + } + }, + stackDownloadFile: async raw => { + try { + const payload = raw as {fileName?: string}; + const fileName = payload.fileName?.trim() ?? ""; + const path = stackPathToFile(fileName); + const blob = await downloadFileContent(path); + const link = document.createElement("a"); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + if (link.parentNode === document.body) { + document.body.removeChild(link); + } + URL.revokeObjectURL(blobUrl); + } catch (err) { + sendFailureNotification(err instanceof Error ? err.message : "Failed to download stack file"); + } + }, + }; + }, [navigate, refreshStack, ucxConnectJob]); + + React.useEffect(() => { + setUcxAuthenticated(false); + }, [ucxConnectJobId]); + + React.useEffect(() => { + if (!shouldAttemptUcxConnection) { + setUcxAuthenticated(false); + } + }, [shouldAttemptUcxConnection]); const pollIntervalMs = React.useMemo(() => { if (uiMode === "Replacement") return 2000; @@ -155,17 +155,17 @@ export default function StackView(): React.ReactNode { return hasLiveJobs ? 5000 : 15000; }, [uiMode, jobs]); - React.useEffect(() => { - if (!id) return; + React.useEffect(() => { + if (!id) return; - const timer = window.setInterval(() => { - refreshStack(); + const timer = window.setInterval(() => { + refreshStack(); }, pollIntervalMs); - return () => { - window.clearInterval(timer); - }; - }, [id, pollIntervalMs, refreshStack]); + return () => { + window.clearInterval(timer); + }; + }, [id, pollIntervalMs, refreshStack]); const restartVm = React.useCallback((job: Job) => { addStandardDialog({ @@ -318,10 +318,10 @@ export default function StackView(): React.ReactNode { return { stack_machines: ctx => { const props = ctx.node.props; - let isPlain = valueToPlain(props["isPlain"] ?? { kind: ValueKind.Null }); + let isPlain = valueToPlain(props["isPlain"] ?? {kind: ValueKind.Null}); if (isPlain == null || typeof isPlain !== "boolean") isPlain = false; let labelFilter: MachinesLabelFilter | undefined = undefined; - const plainLabelFilter = valueToPlain(props["labelFilter"] ?? { kind: ValueKind.Null }); + const plainLabelFilter = valueToPlain(props["labelFilter"] ?? {kind: ValueKind.Null}); if (plainLabelFilter != null && typeof plainLabelFilter === "object" && !Array.isArray(plainLabelFilter)) { const label = (plainLabelFilter as Record)["label"]; const value = (plainLabelFilter as Record)["value"]; @@ -331,7 +331,7 @@ export default function StackView(): React.ReactNode { } return ; + status={status} plain={isPlain} labelFilter={labelFilter} />; }, stack_resources: ctx => { return ; @@ -364,45 +364,45 @@ export default function StackView(): React.ReactNode { {id && stackState.error ?

Could not load stack: {stackState.error.why}

: null} {id && !stackState.loading && !stackState.error && !stack ?

Stack not found.

: null} - {!stack || (uiMode === "Replacement" && ucxAuthenticated) ? null : ( - <> + {!stack || (uiMode === "Replacement" && ucxAuthenticated) ? null : ( + <> - - )} - - {stack && shouldAttemptUcxConnection ? ( -
- { - const accessToken = await Client.receiveAccessTokenOrRefreshIt(); - const project = getStoredProject() ?? ""; - return `${accessToken}\n${project}`; - }} - sysHello={() => JSON.stringify({jobId: ucxConnectJobId})} - rpcHandlers={ucxRpcHandlers} + restartVm={restartVm} /> + + )} + + {stack && shouldAttemptUcxConnection ? ( +
+ { + const accessToken = await Client.receiveAccessTokenOrRefreshIt(); + const project = getStoredProject() ?? ""; + return `${accessToken}\n${project}`; + }} + sysHello={() => JSON.stringify({jobId: ucxConnectJobId})} + rpcHandlers={ucxRpcHandlers} components={ucxComponentRegistry} - onConnected={() => setUcxAuthenticated(true)} - onDisconnected={() => setUcxAuthenticated(false)} - renderFrame={({content}) => content} - /> -
- ) : null} + onConnected={() => setUcxAuthenticated(true)} + onDisconnected={() => setUcxAuthenticated(false)} + renderFrame={({content}) => content} + /> +
+ ) : null} } />; } function isVirtualMachineJob(job: Job): boolean { - return job.status.resolvedApplication?.invocation.tool.tool?.description.backend === "VIRTUAL_MACHINE"; + return job.status.resolvedApplication?.invocation.tool.tool?.description.backend === "VIRTUAL_MACHINE"; } const ResourcesInStack: React.FunctionComponent<{ status?: StackStatus | null; - openResourcesDialog: (kind: string) => void; + openResourcesDialog: (kind: "jobs" | "licenses" | "publicLinks" | "publicIps" | "networks") => void; }> = ({openResourcesDialog, status}) => { const jobs = status?.jobs ?? []; @@ -477,79 +477,79 @@ const MachinesInStack: React.FunctionComponent<{ - {filteredJobs.map(job => { - const isVm = isVirtualMachineJob(job); - const isTerminalState = isJobStateTerminal(job.status.state); - const isSuspended = job.status.state === "SUSPENDED"; - - const actionItems: VmActionItem[] = []; - if (isVm && !isTerminalState && !isSuspended) { - actionItems.push({ - key: "restart", - value: "Restart", - icon: "heroArrowPath", - color: "warningMain", - }); - } + {filteredJobs.map(job => { + const isVm = isVirtualMachineJob(job); + const isTerminalState = isJobStateTerminal(job.status.state); + const isSuspended = job.status.state === "SUSPENDED"; + + const actionItems: VmActionItem[] = []; + if (isVm && !isTerminalState && !isSuspended) { + actionItems.push({ + key: "restart", + value: "Restart", + icon: "heroArrowPath", + color: "warningMain", + }); + } - const powerTone: "success" | "warning" | "neutral" = isTerminalState - ? "neutral" - : isSuspended - ? "success" - : "warning"; - - return - -
{job.specification.name ?? shortUUID(job.id)}
-
- ID: {shortUUID(job.id)} - {" | "} - Started: {job.status.startedAt ? dateToString(job.status.startedAt) : "Pending"} -
-
- -
{stateToTitle(job.status.state)}
-
- -
{job.specification.product.id}
-
- - - {!isVm ? null : ( - { - if (isTerminalState) return; - suspendVm(job); - }} - menuItems={actionItems} - onSelectMenuItem={item => { - if (item.key === "restart") restartVm(job); - }} - dropdownWidth="220px" - /> - )} - - - - - - -
; - })} + const powerTone: "success" | "warning" | "neutral" = isTerminalState + ? "neutral" + : isSuspended + ? "success" + : "warning"; + + return + +
{job.specification.name ?? shortUUID(job.id)}
+
+ ID: {shortUUID(job.id)} + {" | "} + Started: {job.status.startedAt ? dateToString(job.status.startedAt) : "Pending"} +
+
+ +
{stateToTitle(job.status.state)}
+
+ +
{job.specification.product.id}
+
+ + + {!isVm ? null : ( + { + if (isTerminalState) return; + suspendVm(job); + }} + menuItems={actionItems} + onSelectMenuItem={item => { + if (item.key === "restart") restartVm(job); + }} + dropdownWidth="220px" + /> + )} + + + + + + +
; + })} ; diff --git a/frontend-web/webclient/app/Stacks/StacksBrowse.tsx b/frontend-web/webclient/app/Stacks/StacksBrowse.tsx index ea3ab8013c..17a81c1691 100644 --- a/frontend-web/webclient/app/Stacks/StacksBrowse.tsx +++ b/frontend-web/webclient/app/Stacks/StacksBrowse.tsx @@ -109,7 +109,8 @@ export default function StacksBrowse(): React.ReactNode { } row.title.append(ResourceBrowser.defaultTitleRenderer(stack.id, row)); - row.stat1.textContent = stack.type ?? stack.name ?? "Unknown"; + /* Note(Jonas), FIXME: I don't know what `stack.name` should be referring to. */ + row.stat1.textContent = stack.type ?? stack["name"] ?? "Unknown"; row.stat2.textContent = dateToString(stack.createdAt); }); diff --git a/frontend-web/webclient/app/Terminal/Container.tsx b/frontend-web/webclient/app/Terminal/Container.tsx index ac7a4f1b8c..0e74e360b9 100644 --- a/frontend-web/webclient/app/Terminal/Container.tsx +++ b/frontend-web/webclient/app/Terminal/Container.tsx @@ -1,8 +1,7 @@ import * as React from "react"; -import {TerminalAction, TerminalState, TerminalTab, useTerminalDispatcher, useTerminalState} from "@/Terminal/State"; +import {terminalClose, terminalCloseTab, terminalSelectTab, TerminalState, TerminalTab, useTerminalState} from "@/Terminal/State"; import {useCallback, useEffect, useRef, useMemo, useState} from "react"; import {Icon, Truncate} from "@/ui-components"; -import {Feature, hasFeature} from "@/Features"; import {injectStyle} from "@/Unstyled"; import {noopCall, useCloudAPI} from "@/Authentication/DataHook"; import {BulkResponse} from "@/UCloud"; @@ -14,6 +13,8 @@ import {getCssPropertyValue} from "@/Utilities/StylingUtilities"; import {CSSVarCurrentSidebarStickyWidth} from "@/ui-components/List"; import {Tab} from "@/Editor/Editor"; import {Operation, Operations, ShortcutKey} from "@/ui-components/Operation"; +import {useDispatch} from "react-redux"; +import {Dispatch, PayloadAction} from "@reduxjs/toolkit"; const Wrapper = injectStyle("wrapper", k => ` ${k} { @@ -84,7 +85,7 @@ const Wrapper = injectStyle("wrapper", k => ` export const TerminalContainer: React.FunctionComponent = () => { const state = useTerminalState(); - const dispatch = useTerminalDispatcher(); + const dispatch = useDispatch(); const termSizeSaved = useRef(400); @@ -134,7 +135,7 @@ export const TerminalContainer: React.FunctionComponent = () => { } }, [state.activeTab]); - const [operations, setOperations] = useState[]>([]); + const [operations, setOperations] = useState[]>([]); const openTabOperations = React.useCallback((idx: number, position: {x: number; y: number;}) => { const ops = tabOperations(dispatch, idx, state); setOperations(ops); @@ -174,8 +175,7 @@ export const TerminalContainer: React.FunctionComponent = () => { forceEvaluationOnOpen={true} openFnRef={openTabOperationWindow} selected={[]} - extra={null} - row={42} + extra={undefined} hidden location={"IN_ROW"} /> @@ -193,13 +193,13 @@ export const TerminalContainer: React.FunctionComponent = () => { ; }; -function tabOperations(dispatch: (action: TerminalAction) => void, tabIdx: number, state: TerminalState): Operation[] { +function tabOperations(dispatch: Dispatch, tabIdx: number, state: TerminalState): Operation[] { return [ { text: "Close tab", enabled: () => true, onClick() { - dispatch({type: "TerminalCloseTab", payload: {tabIdx: tabIdx}}); + dispatch(terminalCloseTab({tabIdx: tabIdx})); }, "shortcut": ShortcutKey.A, }, @@ -208,7 +208,7 @@ function tabOperations(dispatch: (action: TerminalAction) => void, tabIdx: numbe onClick() { for (let idx = state.tabs.length - 1; idx >= 0; idx--) { if (idx === tabIdx) continue; - dispatch({type: "TerminalCloseTab", payload: {tabIdx: idx}}); + dispatch(terminalCloseTab({tabIdx: idx})); } }, "shortcut": ShortcutKey.B, @@ -216,11 +216,11 @@ function tabOperations(dispatch: (action: TerminalAction) => void, tabIdx: numbe { text: "Close to the right", enabled: () => true /* todo */, onClick() { for (let i = state.tabs.length - 1; i > tabIdx; i--) { - dispatch({type: "TerminalCloseTab", payload: {tabIdx: i}}); + dispatch(terminalCloseTab({tabIdx: i})) } if (tabIdx < state.activeTab) { - dispatch({type: "TerminalSelectTab", payload: {tabIdx: tabIdx}}) + terminalSelectTab({tabIdx}) } }, "shortcut": ShortcutKey.C, @@ -230,7 +230,7 @@ function tabOperations(dispatch: (action: TerminalAction) => void, tabIdx: numbe for (let i = state.tabs.length - 1; i >= 0; i--) { dispatch({type: "TerminalCloseTab", payload: {tabIdx: i}}); } - dispatch({type: "TerminalClose"}); + dispatch(terminalClose()); }, "shortcut": ShortcutKey.D, }, diff --git a/frontend-web/webclient/app/Terminal/State.ts b/frontend-web/webclient/app/Terminal/State.ts index 4b926f1756..5a0c6fa7c5 100644 --- a/frontend-web/webclient/app/Terminal/State.ts +++ b/frontend-web/webclient/app/Terminal/State.ts @@ -1,6 +1,6 @@ -import {useDispatch, useSelector} from "react-redux"; +import {useSelector} from "react-redux"; import {randomUUID} from "@/UtilityFunctions"; -import {Action, PayloadAction} from "@reduxjs/toolkit"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; export interface TerminalTab { title: string; @@ -14,18 +14,6 @@ export interface TerminalState { open: boolean; } -type TerminalOpenTab = PayloadAction<{tab: TerminalTab}, "TerminalOpenTab">; - -type TerminalCloseTab = PayloadAction<{tabIdx: number}, "TerminalCloseTab">; - -type TerminalOpen = Action<"TerminalOpen">; - -type TerminalClose = Action<"TerminalClose">; - -type TerminalSelectTab = PayloadAction<{tabIdx: number}, "TerminalSelectTab">; - -export type TerminalAction = TerminalOpenTab | TerminalCloseTab | TerminalOpen | TerminalClose | TerminalSelectTab; - export function initTerminalState(): TerminalState { return { activeTab: -1, @@ -34,43 +22,40 @@ export function initTerminalState(): TerminalState { }; } -export function terminalReducer(state: TerminalState = initTerminalState(), action: TerminalAction): TerminalState { - switch (action.type) { - case "TerminalOpen": { - return {...state, open: true}; - } - - case "TerminalClose": { - return {...state, open: false}; - } - - case "TerminalOpenTab": { +const terminalSlice = createSlice({ + name: "terminal", + initialState: initTerminalState(), + reducers: { + terminalOpen(state) { + state.open = true; + }, + terminalClose(state) { + state.open = false; + }, + terminalOpenTab(state, action: PayloadAction<{tab: TerminalTab}>) { const tabWithId = {...action.payload.tab}; tabWithId.uniqueId = randomUUID(); - const tabs = [...state.tabs, tabWithId]; - return {...state, tabs, activeTab: state.activeTab < 0 ? 0 : state.activeTab}; - } - - case "TerminalCloseTab": { + state.tabs = [...state.tabs, tabWithId]; + state.activeTab = state.activeTab < 0 ? 0 : state.activeTab; + }, + terminalCloseTab(state, action: PayloadAction<{tabIdx: number}>) { const tabs = [...state.tabs]; tabs.splice(action.payload.tabIdx, 1); const newActiveTab = Math.min(state.tabs.length - 2, state.activeTab); - return {...state, tabs, activeTab: newActiveTab, open: state.open && tabs.length > 0}; - } - - case "TerminalSelectTab": { - return {...state, activeTab: action.payload.tabIdx, open: true}; + state.tabs = tabs; + state.activeTab = newActiveTab; + state.open = state.open && tabs.length > 0; + }, + terminalSelectTab(state, action: PayloadAction<{tabIdx: number}>) { + state.activeTab = action.payload.tabIdx; + state.open = true; } - - default: - return state; } -} +}); -export function useTerminalDispatcher(): (action: TerminalAction) => void { - return useDispatch(); -} +export const {terminalClose, terminalCloseTab, terminalOpen, terminalOpenTab, terminalSelectTab} = terminalSlice.actions; +export const terminalReducer = terminalSlice.reducer; export function useTerminalState(): TerminalState { return useSelector(it => it.terminal); diff --git a/frontend-web/webclient/app/Types/index.ts b/frontend-web/webclient/app/Types/index.ts index 946d249f55..8aed4d65c6 100644 --- a/frontend-web/webclient/app/Types/index.ts +++ b/frontend-web/webclient/app/Types/index.ts @@ -1,6 +1,5 @@ import {emptyPage} from "@/Utilities/PageUtilities"; import {PayloadAction} from "@reduxjs/toolkit"; -import {Action} from "redux"; declare global { const DEVELOPMENT_ENV: boolean; @@ -54,5 +53,4 @@ export function arrayToPage(items: T[], itemsPerPage = 50, page = 0): Page }; } -export type SetLoadingAction = PayloadAction<{loading: boolean}, T>; export type Error = PayloadAction<{error?: string, statusCode?: number}, T>; \ No newline at end of file diff --git a/frontend-web/webclient/app/UCX/UcxView.tsx b/frontend-web/webclient/app/UCX/UcxView.tsx index db54f231f2..8e50fcc3bd 100644 --- a/frontend-web/webclient/app/UCX/UcxView.tsx +++ b/frontend-web/webclient/app/UCX/UcxView.tsx @@ -405,7 +405,7 @@ const UcxView: React.FunctionComponent = ({ if (socket.readyState !== WebSocket.OPEN) { return; } - socket.send(bytes); + socket.send(bytes as Uint8Array); }); socket.onopen = () => { @@ -713,14 +713,14 @@ const baseComponents: UcxComponentRegistry = { if (!text) return null; return , + h1: p => , + h2: p => , + h3: p => , + h4: p => , + h5: p => , + h6: p => , + pre: p => , }} allowedElements={["h1", "h2", "h3", "h4", "h5", "h6", "br", "a", "p", "strong", "b", "i", "em", "ul", "ol", "li", "pre", "code"]} children={text as string} @@ -1839,11 +1839,11 @@ const FieldLabel = ({children, onClick}: React.PropsWithChildren<{onClick?: Reac return
{children}
; }; -function MarkdownLink(props: {href?: string; children: React.ReactNode & React.ReactNode[]}) { +function MarkdownLink(props: {href?: string; children: React.ReactNode}) { return {props.children}; } -function MarkdownHeading(props: {children: React.ReactNode & React.ReactNode[]}) { +function MarkdownHeading(props: {children: React.ReactNode}) { return {props.children}; } diff --git a/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx b/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx index 42a910b238..0f1491a694 100644 --- a/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx +++ b/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx @@ -122,7 +122,7 @@ class FileCollectionsApi extends ResourceApi>[] { + retrieveOperations(): Operation>[] { const baseOperations = super.retrieveOperations(); const permissions = baseOperations.find(it => it.tag === PERMISSIONS_TAG); if (permissions) { diff --git a/frontend-web/webclient/app/UCloud/FilesApi.tsx b/frontend-web/webclient/app/UCloud/FilesApi.tsx index 0a5a5f8887..a3c9d366ae 100644 --- a/frontend-web/webclient/app/UCloud/FilesApi.tsx +++ b/frontend-web/webclient/app/UCloud/FilesApi.tsx @@ -202,7 +202,7 @@ class FilesApi extends ResourceApi & ExtraFileCallbacks> = { + renderer: ItemRenderer & ExtraFileCallbacks> = { }; private defaultRetrieveFlags: Partial = { @@ -237,10 +237,10 @@ class FilesApi extends ResourceApi } - public retrieveOperations(): Operation & ExtraFileCallbacks>[] { + public retrieveOperations(): Operation>[] { const base = super.retrieveOperations() .filter(it => it.tag !== CREATE_TAG && it.tag !== PERMISSIONS_TAG && it.tag !== DELETE_TAG); - const ourOps: Operation & ExtraFileCallbacks>[] = [ + const ourOps: Operation & ExtraFileCallbacks>[] = [ { text: "Use this folder", primary: true, @@ -707,7 +707,8 @@ class FilesApi extends ResourceApi>[]); } public transfer(request: BulkRequest): APICallParameters> { @@ -877,7 +878,7 @@ function handleSyncthingWarning(files: UFile[], cb: ExtraFileCallbacks, op: () = } } -function synchronizationOpText(files: UFile[], callbacks: ResourceBrowseCallbacks & ExtraFileCallbacks): string { +function synchronizationOpText(files: UFile[], callbacks: ResourceBrowseCallbacks & ExtraFileCallbacks): string { const devices: SyncthingDevice[] = callbacks.syncthingConfig?.devices ?? []; if (devices.length === 0) return "Sync setup"; @@ -906,7 +907,7 @@ function isAnySynchronized(files: UFile[], callbacks: ExtraFileCallbacks): boole return synchronizedFolders.find(it => it.ucloudPath && filePaths.includes(it.ucloudPath)) != null; } -function synchronizationOpEnabled(isDir: boolean, files: UFile[], cb: ResourceBrowseCallbacks & ExtraFileCallbacks): boolean | string { +function synchronizationOpEnabled(isDir: boolean, files: UFile[], cb: ResourceBrowseCallbacks & ExtraFileCallbacks): boolean | string { const support = cb.collection?.status.resolvedSupport?.support; if (!support) return false; @@ -931,7 +932,7 @@ function synchronizationOpEnabled(isDir: boolean, files: UFile[], cb: ResourceBr return true; } -async function synchronizationOpOnClick(files: UFile[], cb: ResourceBrowseCallbacks & ExtraFileCallbacks) { +async function synchronizationOpOnClick(files: UFile[], cb: ResourceBrowseCallbacks & ExtraFileCallbacks) { const synchronized: SyncthingFolder[] = cb.syncthingConfig?.folders ?? []; const resolvedFiles = files.length === 0 ? (cb.directory ? [cb.directory] : []) : files; const allSynchronized = resolvedFiles.every(selected => synchronized.some(it => it.ucloudPath === selected.id)); @@ -1157,7 +1158,7 @@ function isFileFileSizeExceeded(file: UFile) { export function FilePreview({initialFile}: { initialFile: UFile, }): React.ReactNode { - const [openFile, setOpenFile] = useState<[string, string | Uint8Array]>(["", ""]); + const [openFile, setOpenFile] = useState<[string, string | Uint8Array]>(["", ""]); const [previewRequested, setPreviewRequested] = useState(false); const [drive, setDrive] = useState(null); const [renamingFile, setRenamingFile] = useState(); @@ -1320,8 +1321,8 @@ export function FilePreview({initialFile}: { } } - window.addEventListener(FileWriteFailure, revertLocalSave); - window.setTimeout(() => window.removeEventListener(FileWriteFailure, revertLocalSave), 30_000); + window.addEventListener(FileWriteFailure, {handleEvent: revertLocalSave}); + window.setTimeout(() => window.removeEventListener(FileWriteFailure, {handleEvent: revertLocalSave}), 30_000); }, [vfs, node]); useEffect(() => { @@ -1347,7 +1348,7 @@ export function FilePreview({initialFile}: { } }, [onSave, requestPreviewToggle]); - const onOpenFile = useCallback((path: string, data: string | Uint8Array) => { + const onOpenFile = useCallback((path: string, data: string | Uint8Array) => { setPreviewRequested(false); setOpenFile(file => { const [currentPath] = file; @@ -1419,7 +1420,7 @@ export function FilePreview({initialFile}: { return success; }, []); - const operations = useCallback((file?: VirtualFile): Operation[] => { + const operations = useCallback((file?: VirtualFile): Operation[] => { const reload = () => { editorRef.current?.invalidateTree(removeTrailingSlash(getParentPath(initialFile.id))); } diff --git a/frontend-web/webclient/app/UCloud/JobsApi.tsx b/frontend-web/webclient/app/UCloud/JobsApi.tsx index 0c3880369d..1319a48552 100644 --- a/frontend-web/webclient/app/UCloud/JobsApi.tsx +++ b/frontend-web/webclient/app/UCloud/JobsApi.tsx @@ -304,7 +304,7 @@ class JobApi extends ResourceApi & {isModal: boolean}>[] { + retrieveOperations(): Operation>[] { const baseOperations = super.retrieveOperations(); const deleteOperation = baseOperations.find(it => it.tag === DELETE_TAG)!; deleteOperation.text = "Stop"; @@ -323,7 +323,7 @@ class JobApi extends ResourceApi & {isModal: boolean}>[] = [{ + const ourOps: Operation>[] = [{ // Re-run app enabled: (selected, cb) => { const isSyncthing = isSyncthingApp(selected[0]); diff --git a/frontend-web/webclient/app/UCloud/LicenseApi.tsx b/frontend-web/webclient/app/UCloud/LicenseApi.tsx index de8f5dca35..399c00bd56 100644 --- a/frontend-web/webclient/app/UCloud/LicenseApi.tsx +++ b/frontend-web/webclient/app/UCloud/LicenseApi.tsx @@ -56,7 +56,7 @@ class LicenseApi extends ResourceApi>[] { + retrieveOperations(): Operation>[] { const res = super.retrieveOperations(); const createOp = res.find(it => it.tag === CREATE_TAG); if (createOp) createOp.text = "Activate license"; diff --git a/frontend-web/webclient/app/UCloud/Messages.ts b/frontend-web/webclient/app/UCloud/Messages.ts index 8c99b4b5fa..8a001f29b1 100644 --- a/frontend-web/webclient/app/UCloud/Messages.ts +++ b/frontend-web/webclient/app/UCloud/Messages.ts @@ -634,7 +634,7 @@ class Simple implements UBinaryType { } encodeToJson() { - return { fie: this.fie, hund: this.hund, enumeration: TopCompanion.serialName(this.enumeration) } + return {fie: this.fie, hund: this.hund, enumeration: TopCompanion.serialName(this.enumeration)} } static create( @@ -698,7 +698,7 @@ function useAllocator(block: (allocator: BinaryAllocator) => R): R { return block(allocator); } -export function loadMessage(ctor: { new(b: BufferAndOffset): T }, view: DataView) { +export function loadMessage(ctor: {new(b: BufferAndOffset): T}, view: DataView) { const alloc = BinaryAllocator.fromView(view); return new ctor(alloc.root()); } @@ -714,7 +714,7 @@ export function messageTest() { }); useAllocator(alloc => { - const decoded = SimpleCompanion.decodeFromJson(alloc, { fie: 1337, hund: "gamer", enumeration: "Eyepatch" }); + const decoded = SimpleCompanion.decodeFromJson(alloc, {fie: 1337, hund: "gamer", enumeration: "Eyepatch"}); console.log(decoded.encodeToJson()); console.log(alloc.slicedBuffer()); }) diff --git a/frontend-web/webclient/app/UCloud/MetadataNamespaceApi.tsx b/frontend-web/webclient/app/UCloud/MetadataNamespaceApi.tsx index 653554e5d0..9b74f6abb9 100644 --- a/frontend-web/webclient/app/UCloud/MetadataNamespaceApi.tsx +++ b/frontend-web/webclient/app/UCloud/MetadataNamespaceApi.tsx @@ -218,7 +218,7 @@ class MetadataNamespaceApi extends ResourceApi>[] { + retrieveOperations(): Operation>[] { return super.retrieveOperations(); } diff --git a/frontend-web/webclient/app/UCloud/NetworkIPApi.tsx b/frontend-web/webclient/app/UCloud/NetworkIPApi.tsx index 555eab60ac..dcdfea04ab 100644 --- a/frontend-web/webclient/app/UCloud/NetworkIPApi.tsx +++ b/frontend-web/webclient/app/UCloud/NetworkIPApi.tsx @@ -116,7 +116,7 @@ class NetworkIPApi extends ResourceApi>[] { + public retrieveOperations(): Operation>[] { const ops = super.retrieveOperations(); const create = ops.find(it => it.tag === "create"); if (create) { diff --git a/frontend-web/webclient/app/UCloud/PrivateNetworkApi.tsx b/frontend-web/webclient/app/UCloud/PrivateNetworkApi.tsx index 9d574ce8ab..c900aef53d 100644 --- a/frontend-web/webclient/app/UCloud/PrivateNetworkApi.tsx +++ b/frontend-web/webclient/app/UCloud/PrivateNetworkApi.tsx @@ -80,7 +80,7 @@ class PrivateNetworkApi extends ResourceApi< super("private-networks"); } - public retrieveOperations(): Operation>[] { + public retrieveOperations(): Operation>[] { const ops = super.retrieveOperations(); const create = ops.find(it => it.tag === CREATE_TAG); if (create) { diff --git a/frontend-web/webclient/app/UCloud/ProvidersApi.tsx b/frontend-web/webclient/app/UCloud/ProvidersApi.tsx index 3080ab1fe8..7ae56a9f72 100644 --- a/frontend-web/webclient/app/UCloud/ProvidersApi.tsx +++ b/frontend-web/webclient/app/UCloud/ProvidersApi.tsx @@ -23,15 +23,12 @@ import {ListRowStat} from "@/ui-components/List"; import {ResourceProperties} from "@/Resource/Properties"; import TitledCard from "@/ui-components/HighlightedCard"; import {doNothing} from "@/UtilityFunctions"; -import {apiUpdate, InvokeCommand, useCloudAPI, useCloudCommand} from "@/Authentication/DataHook"; -import * as UCloud from "@/UCloud/index"; -import {BulkRequest, PageV2} from "@/UCloud/index"; -import {useToggleSet} from "@/Utilities/ToggleSet"; -import {Operation, Operations, ShortcutKey} from "@/ui-components/Operation"; +import {apiUpdate} from "@/Authentication/DataHook"; +import {BulkRequest} from "@/UCloud/index"; +import {Operation, ShortcutKey} from "@/ui-components/Operation"; import {Client} from "@/Authentication/HttpClientInstance"; import {ResourcePermissionEditor} from "@/Resource/PermissionEditor"; import {dialogStore} from "@/Dialog/DialogStore"; -import {emptyPageV2} from "@/Utilities/PageUtilities"; import {MachineView} from "@/Products/Products"; export interface ProviderSpecification extends ResourceSpecification { @@ -58,10 +55,6 @@ export interface Provider extends Resource void; -} - class ProviderApi extends ResourceApi { routingNamespace = "providers"; @@ -88,7 +81,7 @@ class ProviderApi extends ResourceApi>[] { + public retrieveOperations(): Operation>[] { return [ { text: "Create " + this.title.toLowerCase(), diff --git a/frontend-web/webclient/app/UCloud/ResourceApi.tsx b/frontend-web/webclient/app/UCloud/ResourceApi.tsx index 483a3e20e6..e8d6d1f2b8 100644 --- a/frontend-web/webclient/app/UCloud/ResourceApi.tsx +++ b/frontend-web/webclient/app/UCloud/ResourceApi.tsx @@ -156,11 +156,12 @@ export function findSupport( ?.find(it => it.product.name === ref.id && it.product.category.name === ref.category) ?? null as any; } -export interface ResourceBrowseCallbacks { +export interface ResourceBrowseCallbacks { commandLoading: boolean; invokeCommand: InvokeCommand; + isModal?: boolean; reload: () => void; - api: ResourceApi; + api: ResourceApi; isCreating: boolean; startCreation?: () => void; cancelCreation?: () => void; @@ -224,16 +225,16 @@ export abstract class ResourceApi void; closeProperties?: () => void; - api: ResourceApi; + api: ResourceApi; embedded?: EmbeddedSettings; - }> = props => } /> + }> = props => } />} /> protected constructor(namespace: string) { this.namespace = namespace; this.baseContext = "/api/" + namespace.replace(".", "/") + "/"; } - public retrieveOperations(): Operation>[] { + public retrieveOperations(): Operation>[] { return [ { text: "Back to " + this.titlePlural.toLowerCase(), diff --git a/frontend-web/webclient/app/UCloud/SharesApi.tsx b/frontend-web/webclient/app/UCloud/SharesApi.tsx index 1cc4d532a5..b2468ad0bd 100644 --- a/frontend-web/webclient/app/UCloud/SharesApi.tsx +++ b/frontend-web/webclient/app/UCloud/SharesApi.tsx @@ -18,7 +18,6 @@ import {accounting, BulkRequest, FindByStringId, PaginationRequestV2} from "@/UC import {apiBrowse, apiCreate, apiRetrieve, apiUpdate} from "@/Authentication/DataHook"; import {bulkRequestOf} from "@/UtilityFunctions"; import ProductReference = accounting.ProductReference; -import {ValuePill} from "@/Resource/Filter"; import Icon from "@/ui-components/Icon"; export interface ShareSpecification extends ResourceSpecification { @@ -112,7 +111,7 @@ class ShareApi extends ResourceApi> = { + renderer: ItemRenderer> = { MainTitle({resource}) { if (!resource) return null; /* Note(Jonas): If this is shared by logged-in user, we have access to original drive title */ @@ -137,7 +136,7 @@ class ShareApi extends ResourceApi>[] { + retrieveOperations() { const baseOperations = super.retrieveOperations().filter(op => { return op.tag !== CREATE_TAG; }); @@ -158,7 +157,7 @@ class ShareApi extends ResourceApi>[] = [ { text: "Accept", icon: "check", @@ -221,6 +220,7 @@ class ShareApi extends ResourceApi): APICallParameters { diff --git a/frontend-web/webclient/app/UserSettings/Avataaar.tsx b/frontend-web/webclient/app/UserSettings/Avataaar.tsx index b116afe06c..52f9a5303d 100644 --- a/frontend-web/webclient/app/UserSettings/Avataaar.tsx +++ b/frontend-web/webclient/app/UserSettings/Avataaar.tsx @@ -49,7 +49,7 @@ function Modification(): React.ReactNode { onClick={async () => { SimpleAvatarComponentCache.deleteCachedAvatar(Client.username!); avatarState.setAvatar(Client.username!, avatar); - dispatch(await saveAvatar(avatar)); + await saveAvatar(dispatch, avatar); }} mt="5px" mb="5px" @@ -160,7 +160,7 @@ function Modification(): React.ReactNode { try { const r = await promises.makeCancelable(Client.get(findAvatarQuery, undefined)).promise; setAvatar(r.response); - } catch (e) { + } catch (e: any) { if (!e.isCanceled) sendFailureNotification(errorMessageOrDefault(e, "An error occurred fetching current Avatar")); } finally { diff --git a/frontend-web/webclient/app/UserSettings/Redux.ts b/frontend-web/webclient/app/UserSettings/Redux.ts index 51919ea570..f701d4d16d 100644 --- a/frontend-web/webclient/app/UserSettings/Redux.ts +++ b/frontend-web/webclient/app/UserSettings/Redux.ts @@ -1,60 +1,45 @@ import {Client} from "@/Authentication/HttpClientInstance"; -import {Action} from "redux"; - import {findAvatarQuery, saveAvatarQuery} from "@/Utilities/AvatarUtilities"; import {errorMessageOrDefault} from "@/UtilityFunctions"; import {initAvatar} from "@/DefaultObjects"; import {AvatarType} from "@/AvataaarLib"; -import {PayloadAction} from "@reduxjs/toolkit"; -import {sendFailureNotification, sendNotification, sendSuccessNotification} from "@/Notifications"; - -export type AvatarActions = SaveAvataaar | SetAvatarError; - -type SaveAvataaar = PayloadAction<{avatar: AvatarType, loading: true}, typeof AVATAR_SAVE>; -function saveAvataaar(avatar: AvatarType): SaveAvataaar { - return { - type: AVATAR_SAVE, - payload: {avatar, loading: true} - }; -} +import {createSlice, Dispatch, PayloadAction} from "@reduxjs/toolkit"; +import {sendFailureNotification, sendSuccessNotification} from "@/Notifications"; -export async function saveAvatar(avatar: AvatarType): Promise { +export async function saveAvatar(dispatch: Dispatch, avatar: AvatarType): Promise { try { await Client.post(saveAvatarQuery, avatar, undefined); + dispatch(avatarSave(avatar)); sendSuccessNotification("Avatar updated"); - return saveAvataaar(avatar); } catch (e) { sendFailureNotification(errorMessageOrDefault(e, "An error occurred saving the avatar")); - return setAvatarError(); } } -type SetAvatarError = Action; -export function setAvatarError(): SetAvatarError { - return { - type: AVATAR_ERROR, - }; -} - -export async function findAvatar(): Promise { +export async function findAvatar(): Promise | null> { try { const res = await Client.get(findAvatarQuery, undefined); - return saveAvataaar(res.response); + return avatarSave(res.response); } catch (e) { sendFailureNotification(`Fetching avatar: ${errorMessageOrDefault(e, "An error occurred fetching your avatar.")}`); return null; } } -export const AVATAR_SAVE = "AVATAR_SAVE"; -export const AVATAR_ERROR = "AVATAR_ERROR"; - -export const avatarReducer = (state: AvatarType = initAvatar(), action: AvatarActions) => { - switch (action.type) { - case AVATAR_SAVE: - return {...state, ...action.payload.avatar, loading: false}; - case AVATAR_ERROR: - default: - return state; +const avatarSlice = createSlice({ + name: "avatar", + initialState: initAvatar(), + reducers: { + avatarSave(state, action: PayloadAction) { + for (const key of Object.keys(action.payload)) { + state[key] = action.payload[key]; + } + }, + avatarError(state, action: PayloadAction) { + state.error = action.payload; + } } -}; +}); + +export const {avatarError, avatarSave} = avatarSlice.actions; +export const avatarReducer = avatarSlice.reducer; diff --git a/frontend-web/webclient/app/UserSettings/TwoFactorSetup.tsx b/frontend-web/webclient/app/UserSettings/TwoFactorSetup.tsx index 3bcfce73a5..adb33a9a17 100644 --- a/frontend-web/webclient/app/UserSettings/TwoFactorSetup.tsx +++ b/frontend-web/webclient/app/UserSettings/TwoFactorSetup.tsx @@ -1,5 +1,4 @@ import {Client} from "@/Authentication/HttpClientInstance"; -import {SetStatusLoading} from "@/Navigation/Redux"; import * as React from "react"; import {Button, Divider, ExternalLink, Flex, Input} from "@/ui-components"; import Box from "@/ui-components/Box"; @@ -11,13 +10,14 @@ import {SettingsSection} from "./SettingsComponents"; import googlePlay from "@/Assets/Images/google-play-badge.png"; import appStore from "@/Assets/Images/app-store-badge.png"; import {sendFailureNotification} from "@/Notifications"; +import {setStatusLoading} from "@/Navigation/Redux"; interface TwoFactorSetupProps { loading: boolean; mustActivate2fa: boolean; } -export class TwoFactorSetup extends React.Component { +export class TwoFactorSetup extends React.Component<{setLoading: (loading: boolean) => void;} & TwoFactorSetupProps, TwoFactorSetupState> { public state = TwoFactorSetup.initialState(); public componentDidMount() { @@ -44,7 +44,7 @@ export class TwoFactorSetup extends React.Component ({isConnectedToAccount: res.response.connected})); - } catch (res) { + } catch (res: any) { const why: string = res.response.why ?? ""; sendFailureNotification(`Could not fetch 2FA status. ${why}`); } finally { @@ -194,7 +194,7 @@ export class TwoFactorSetup extends React.Component ({isConnectedToAccount: true})); - } catch (res) { + } catch (res: any) { const response = res.response; const why: string = response?.why ?? "Could not submit verification code. Try again later"; sendFailureNotification(why); diff --git a/frontend-web/webclient/app/UserSettings/UserSettings.tsx b/frontend-web/webclient/app/UserSettings/UserSettings.tsx index 9ed5b36a92..2fc4e0ee32 100644 --- a/frontend-web/webclient/app/UserSettings/UserSettings.tsx +++ b/frontend-web/webclient/app/UserSettings/UserSettings.tsx @@ -1,6 +1,6 @@ import {Client} from "@/Authentication/HttpClientInstance"; import {MainContainer} from "@/ui-components/MainContainer"; -import {setLoading, usePage} from "@/Navigation/Redux"; +import {setStatusLoading, usePage} from "@/Navigation/Redux"; import * as React from "react"; import {useDispatch, useSelector} from "react-redux"; import {Box, Flex} from "@/ui-components"; @@ -25,7 +25,7 @@ function UserSettings(): React.ReactNode { const dispatch = useDispatch(); const setHeaderLoading = React.useCallback((loading: boolean) => { - dispatch(setLoading(loading)); + dispatch(setStatusLoading(loading)); }, [dispatch]); const mustActivate2fa = @@ -57,12 +57,12 @@ function UserSettings(): React.ReactNode { header={User settings} main={( - + {mustActivate2fa ? twoFactorSetup : ( <> - - + + @@ -83,7 +83,7 @@ function UserSettings(): React.ReactNode { setLoading={setHeaderLoading} setRefresh={fn => refreshFunctionCache.setRefreshFunction(fn ?? (() => undefined))} /> - + )} diff --git a/frontend-web/webclient/app/Utilities/ReduxHooks.ts b/frontend-web/webclient/app/Utilities/ReduxHooks.ts index 707487b8ee..f9c70e0b40 100644 --- a/frontend-web/webclient/app/Utilities/ReduxHooks.ts +++ b/frontend-web/webclient/app/Utilities/ReduxHooks.ts @@ -1,8 +1,8 @@ -import {useDispatch, useSelector} from "react-redux"; +import {EqualityFn, useDispatch, useSelector} from "react-redux"; import {useCallback} from "react"; import {ProjectCache} from "@/Project"; import * as AppStore from "@/Applications/AppStoreApi"; -import {PayloadAction} from "@reduxjs/toolkit"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; export interface HookStore { uploaderVisible?: boolean; @@ -16,26 +16,17 @@ export interface HookStore { sidebarStickyWidth?: number; } -type Action = GenericSetAction | GenericMergeAction; - -export type GenericSetAction = PayloadAction<{ - property: string; - newValue?: ValueOrSetter; - defaultValue: any; -}, "GENERIC_SET"> - -export type GenericMergeAction = PayloadAction<{ - property: string; - newValue?: any; -}, "GENERIC_MERGE"> +function initialState(): HookStore { + return {}; +} export type ValueOrSetter = T | ((oldValue: T) => T); export function useGlobal( property: Property, defaultValue: NonNullable, - equalityFn?: (left: NonNullable, right: NonNullable) => boolean -): [NonNullable, (newValue: ValueOrSetter) => void, (newValue: Partial) => void] { + equalityFn?: EqualityFn +): [NonNullable, (newValue: HookStore[Property]) => void, (newValue: HookStore[Property]) => void] { /* FIXME: this hook causes memory leaks */ const value = useSelector(it => { if (it.hookStore === undefined) return undefined; @@ -44,11 +35,11 @@ export function useGlobal( /* FIXME END */ const dispatch = useDispatch(); const setter = useCallback((newValue: HookStore[Property]) => { - dispatch({type: "GENERIC_SET", payload: {property, newValue, defaultValue}}); + dispatch(genericSet({property, newValue, defaultValue})); }, [dispatch]); const merger = useCallback((newValue: HookStore[Property]) => { - dispatch({type: "GENERIC_MERGE", payload: {property, newValue}}); + dispatch(genericMerge({property, newValue})); }, [dispatch]); return [ @@ -58,36 +49,30 @@ export function useGlobal( ]; } -function reducer(state: HookStore = {}, action: Action): HookStore { - switch (action.type) { - case "GENERIC_SET": { - const newState = {}; - for (const kv of Object.entries(state)) { - const [key, val] = kv; - newState[key] = val; - } +const hookStore = createSlice({ + name: "hookStore", + initialState: initialState(), + reducers: { + genericSet(state, action: PayloadAction<{ + property: string; + newValue?: ValueOrSetter; + defaultValue: any; + }>) { if (typeof action.payload.newValue === "function") { - newState[action.payload.property] = action.payload.newValue(newState[action.payload.property] ?? action.payload.defaultValue); + state[action.payload.property] = action.payload.newValue(state[action.payload.property] ?? action.payload.defaultValue); } else { - newState[action.payload.property] = action.payload.newValue; + state[action.payload.property] = action.payload.newValue; } - return newState; - } + }, - case "GENERIC_MERGE": { - const stateCopy = {}; - for (const kv of Object.entries(state)) { - const [key, val] = kv; - stateCopy[key] = val; - } - stateCopy[action.payload.property] = {...stateCopy[action.payload.property], ...action.payload.newValue}; - return stateCopy; - } - - default: { - return state; + genericMerge(state, action: PayloadAction<{ + property: string; + newValue?: any; + }>) { + state[action.payload.property] = {...state[action.payload.property], ...action.payload.newValue}; } } -} +}); -export default reducer; +export const {genericMerge, genericSet} = hookStore.actions; +export const hookStoreReducer = hookStore.reducer; diff --git a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx index 25d9564eb3..414d8d8174 100644 --- a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx @@ -1,5 +1,5 @@ import {useEffect} from "react"; -import {Action, AnyAction, combineReducers} from "redux"; +import {combineReducers, Reducer} from "redux"; import {dashboardReducer} from "@/Dashboard/Redux"; import {initObject} from "@/DefaultObjects"; @@ -7,10 +7,10 @@ import {statusReducer} from "@/Navigation/Redux"; import * as ProjectRedux from "@/Project/ReduxState"; import {avatarReducer} from "@/UserSettings/Redux"; import {terminalReducer} from "@/Terminal/State"; -import hookStore from "@/Utilities/ReduxHooks"; +import {hookStoreReducer} from "@/Utilities/ReduxHooks"; import {popInReducer} from "@/ui-components/PopIn"; -import sidebar from "@/Applications/Redux/Reducer"; -import {EnhancedStore, ReducersMapObject, configureStore} from "@reduxjs/toolkit"; +import {sidebarReducer} from "@/Applications/Redux/Reducer"; +import {EnhancedStore, PayloadAction, ReducersMapObject, configureStore} from "@reduxjs/toolkit"; import {noopCall} from "@/Authentication/DataHook"; import {providerBrandingReducer} from "@/ProviderBrandings/AutomaticProviderBranding"; import {brandingReducer} from "@/Applications/Branding/AutomaticBranding"; @@ -23,23 +23,28 @@ export type UserActionType = typeof USER_LOGIN | typeof USER_LOGOUT | typeof CON function confStore( initialObject: ReduxObject, - reducers: ReducersMapObject, -): EnhancedStore { + reducers: ReducersMapObject, +): EnhancedStore { const combinedReducers = combineReducers(reducers); - const rootReducer = (state: ReduxObject, action: Action): ReduxObject => { + const rootReducer = (state: ReduxObject | undefined, action: PayloadAction) => { if ([USER_LOGIN, USER_LOGOUT, CONTEXT_SWITCH].some(it => it === action.type)) { state = initObject(); } return combinedReducers(state, action); }; - return configureStore({reducer: rootReducer, preloadedState: initialObject}); + return configureStore({ + reducer: rootReducer, + preloadedState: initialObject + }); } + + export const store = confStore(initObject(), { dashboard: dashboardReducer, status: statusReducer, - hookStore, - sidebar, + hookStore: hookStoreReducer, + sidebar: sidebarReducer, avatar: avatarReducer, terminal: terminalReducer, providerBrandings: providerBrandingReducer, diff --git a/frontend-web/webclient/app/ui-components/Card.tsx b/frontend-web/webclient/app/ui-components/Card.tsx index c29cd68103..a11d0402b0 100644 --- a/frontend-web/webclient/app/ui-components/Card.tsx +++ b/frontend-web/webclient/app/ui-components/Card.tsx @@ -22,8 +22,9 @@ export interface CardProps extends HeightProps, PaddingProps, MinHeightProps { borderWidth?: number | string; - children?: React.ReactNode; onClick?: (e: React.SyntheticEvent) => void; - onContextMenu?: (e: React.SyntheticEvent) => void; + children?: React.ReactNode; + onClick?: (e: React.SyntheticEvent) => void; + onContextMenu?: React.MouseEventHandler; className?: string; style?: CSSProperties; } diff --git a/frontend-web/webclient/app/ui-components/ClickableDropdown.tsx b/frontend-web/webclient/app/ui-components/ClickableDropdown.tsx index 4c721033b2..a1d0a9185a 100644 --- a/frontend-web/webclient/app/ui-components/ClickableDropdown.tsx +++ b/frontend-web/webclient/app/ui-components/ClickableDropdown.tsx @@ -47,7 +47,7 @@ export interface ClickableDropdownProps { /** * Requires `arrowkeyNavigationKey` to be set or that `props.children` are provided. **/ - onSelect?: (el: HTMLElement | undefined) => void; + onSelect?: (el: Element | undefined) => void; chevron?: boolean; overflow?: string; @@ -135,7 +135,7 @@ function ClickableDropdown({ const handleKeyPress: (ev: KeyboardEvent) => void = useCallback((event): void => { if (props.arrowkeyNavigationKey) { const navigationKey = props.arrowkeyNavigationKey ?? "data-active"; - _onKeyDown(event, divRef, counter, navigationKey, props.onSelect, props.hoverColor ?? "primaryLight") + _onKeyDown(event, divRef, counter, navigationKey, props.hoverColor ?? "primaryLight", props.onSelect) } if (event.key === "Escape" && open) { @@ -308,11 +308,11 @@ export default ClickableDropdown; function _onKeyDown( e: KeyboardEvent, - wrapper: React.RefObject, + wrapper: React.RefObject, index: React.RefObject, entryKey: string, - onSelect: ((el: Element | undefined) => void) | undefined, hoverColor: ThemeColor, + onSelect?: ((el: Element | undefined) => void), ) { if (!wrapper.current) return; const isUp = e.key === "ArrowUp"; diff --git a/frontend-web/webclient/app/ui-components/ConfirmationAction.tsx b/frontend-web/webclient/app/ui-components/ConfirmationAction.tsx index c5a36f4a70..809c69b0d0 100644 --- a/frontend-web/webclient/app/ui-components/ConfirmationAction.tsx +++ b/frontend-web/webclient/app/ui-components/ConfirmationAction.tsx @@ -233,7 +233,7 @@ export const ConfirmationButton: React.FunctionComponent void; + onAction?: (actionKey?: string) => Promise; hoverColor?: ThemeColor; disabled?: boolean; }> = props => { diff --git a/frontend-web/webclient/app/ui-components/Icon.tsx b/frontend-web/webclient/app/ui-components/Icon.tsx index 3c70840e11..c109aed1e2 100644 --- a/frontend-web/webclient/app/ui-components/Icon.tsx +++ b/frontend-web/webclient/app/ui-components/Icon.tsx @@ -83,13 +83,16 @@ const Icon: React.FunctionComponent = ({size = 18, squared = true Icon.displayName = "Icon"; // Use to see every available icon in debugging. -export const EveryIcon = (): React.ReactNode => ( - <> - {Object.keys(icons).map((it: IconName, i: number) => - () - )} - -); +export const EveryIcon = (): React.ReactNode => { + const iconNames: IconName[] = Object.keys(icons) as unknown as IconName[]; + return ( + <> + {iconNames.map((it, i) => + () + )} + + ); +} // bug icon diff --git a/frontend-web/webclient/app/ui-components/Markdown.tsx b/frontend-web/webclient/app/ui-components/Markdown.tsx index 84be70764d..f75a444a0b 100644 --- a/frontend-web/webclient/app/ui-components/Markdown.tsx +++ b/frontend-web/webclient/app/ui-components/Markdown.tsx @@ -4,7 +4,7 @@ import ExternalLink from "./ExternalLink"; import SyntaxHighlighter from "react-syntax-highlighter"; import {injectStyle} from "@/Unstyled"; -function CodeBlock(props: {lang?: string; inline?: boolean; children: React.ReactNode & React.ReactNode[]}) { +function CodeBlock(props: {lang?: string; inline?: boolean; children: React.ReactNode}) { if (props.inline === true || !props.lang) return {props.children}; return ( @@ -14,7 +14,7 @@ function CodeBlock(props: {lang?: string; inline?: boolean; children: React.Reac ); } -function LinkBlock(props: {href?: string; children: React.ReactNode & React.ReactNode[]}) { +function LinkBlock(props: {href?: string; children: React.ReactNode} & React.AnchorHTMLAttributes) { return {props.children}; } @@ -22,8 +22,8 @@ function Markdown(props: Options): React.ReactNode { return , + code: p => }} /> } @@ -31,7 +31,7 @@ function Markdown(props: Options): React.ReactNode { export function SimpleMarkdown({children}: React.PropsWithChildren): React.ReactNode { return {p.children} }} allowedElements={["br", "a", "p", "strong", "b", "i", "em"]} children={children as string} diff --git a/frontend-web/webclient/app/ui-components/Operation.tsx b/frontend-web/webclient/app/ui-components/Operation.tsx index a5bbca1762..80960395a1 100644 --- a/frontend-web/webclient/app/ui-components/Operation.tsx +++ b/frontend-web/webclient/app/ui-components/Operation.tsx @@ -75,10 +75,10 @@ export interface Operation { tag?: string; } -export function defaultOperationType( +export function defaultOperationType( location: OperationLocation, - allOperations: Operation[], - op: Operation, + allOperations: Operation[], + op: Operation, ): OperationComponentType { if (op.confirm === true) { return ConfirmationButton; @@ -93,19 +93,19 @@ export function defaultOperationType( } } -const OperationComponent: React.FunctionComponent<{ +function OperationComponent({As, op, selected, all, extra, reasonDisabled, location, onAction, text}: { // eslint-disable-next-line @typescript-eslint/no-explicit-any As: React.ComponentType; - op: Operation; - extra: unknown; - selected: unknown[]; - all?: unknown[]; + op: Operation; + extra: Extra; + selected: T[]; + all?: T[]; reasonDisabled?: string; location: OperationLocation; onAction: () => void; text: string; idx: number; -}> = ({As, op, selected, all, extra, reasonDisabled, location, onAction, text}) => { +}): React.ReactNode { const onClick = useCallback((e?: React.SyntheticEvent) => { if (op.primary === true) e?.stopPropagation(); if (reasonDisabled !== undefined) return; diff --git a/frontend-web/webclient/app/ui-components/PopIn.tsx b/frontend-web/webclient/app/ui-components/PopIn.tsx index 2580672518..15cdca8a83 100644 --- a/frontend-web/webclient/app/ui-components/PopIn.tsx +++ b/frontend-web/webclient/app/ui-components/PopIn.tsx @@ -4,7 +4,7 @@ import Flex from "./Flex"; import Icon from "./Icon"; import {injectStyle} from "@/Unstyled"; import {Spacer} from "./Spacer"; -import {PayloadAction} from "@reduxjs/toolkit"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; function PopIn({hasContent, children}: React.PropsWithChildren<{hasContent: boolean}>): React.ReactNode { return
@@ -35,17 +35,15 @@ const PopInClass = injectStyle("popin-class", k => ` `); export function RightPopIn(): React.ReactNode { - const dispatch = useDispatch(); - const content = useSelector(it => it.popinChild); /* Alternatively, use React.portal */ - return + return dispatch(setPopInChild(null))} />} + left={ setPopInChild({el: undefined})} />} right={content?.onFullScreen ? { content?.onFullScreen?.(); - dispatch(setPopInChild(null)); + setPopInChild({el: undefined}); }} /> : null} /> @@ -58,19 +56,22 @@ export interface PopInArgs { onFullScreen?: () => void; } -type SetPopInChildAction = PayloadAction; -export function setPopInChild(args: PopInArgs | null): SetPopInChildAction { +function initialState(): PopInArgs { return { - type: "SET_POP_IN_CHILD", - payload: args - }; + el: undefined + } } -export const popInReducer = (state: PopInArgs | null = null, action: SetPopInChildAction): PopInArgs | null => { - switch (action.type) { - case "SET_POP_IN_CHILD": - return action.payload; - default: - return state; +const popInSlice = createSlice({ + name: "popIn", + initialState: initialState(), + reducers: { + setPopInChild(state, action: PayloadAction) { + state = action.payload; + } } -}; \ No newline at end of file +}); + +export const {setPopInChild} = popInSlice.actions; + +export const popInReducer = popInSlice.reducer; \ No newline at end of file diff --git a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx index 2a4b7b586c..e6e74e8796 100644 --- a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx +++ b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx @@ -46,7 +46,7 @@ import Flex, {FlexClass} from "./Flex"; import * as Heading from "@/ui-components/Heading"; import {dialogStore} from "@/Dialog/DialogStore"; import {isAdminOrPI} from "@/Project"; -import {callAPI, noopCall} from "@/Authentication/DataHook"; +import {noopCall} from "@/Authentication/DataHook"; import {injectResourceBrowserStyle, ShortcutClass} from "./ResourceBrowserStyle"; import {ASC, DESC, Filter, FilterCheckbox, FilterInput, FilterOption, FilterWithOptions, MultiOption, MultiOptionFilter, SORT_BY, SORT_DIRECTION} from "./ResourceBrowserFilters"; import {sendInformationNotification} from "@/Notifications"; @@ -126,7 +126,7 @@ export interface ResourceBrowseHeaderControls { export type OperationOrGroup = Operation | OperationGroup; -export function isOperation(op: OperationOrGroup): op is Operation { +export function isOperation(op: OperationOrGroup): op is Operation { return !("operations" in op); } @@ -198,7 +198,7 @@ interface ResourceBrowserListenerMap { "renderEmptyPage": (reason: EmptyReason) => void; - "fetchOperations": () => OperationOrGroup[]; + "fetchOperations": () => OperationOrGroup[]; "fetchOperationsCallback": () => unknown | null; "fetchBrowserFeatures": () => ControlDescription[] | undefined; @@ -1598,7 +1598,7 @@ export class ResourceBrowser { const page = this.cachedData[this.currentPath] ?? []; const renderOpIconAndText = ( - op: OperationOrGroup, + op: OperationOrGroup, element: HTMLElement, shortcut?: string, inContextMenu?: boolean @@ -1726,7 +1726,7 @@ export class ResourceBrowser { } const renderOperationsInContextMenu = ( - operations: OperationOrGroup[], + operations: OperationOrGroup[], posX: number, posY: number, counter: number = 1, @@ -3844,7 +3844,7 @@ function ControlsDialog({features, custom}: {features: ResourceBrowseFeatures, c
} -export function controlsOperation(features: ResourceBrowseFeatures, custom?: ControlDescription[]): Operation(features: ResourceBrowseFeatures, custom?: ControlDescription[]): Operation & {hackNotInTheContextMenu: true} { return { diff --git a/frontend-web/webclient/app/ui-components/ScaffoldedForm.tsx b/frontend-web/webclient/app/ui-components/ScaffoldedForm.tsx index 865dc8de5d..988eb0a9d0 100644 --- a/frontend-web/webclient/app/ui-components/ScaffoldedForm.tsx +++ b/frontend-web/webclient/app/ui-components/ScaffoldedForm.tsx @@ -53,8 +53,8 @@ export interface ImageElement extends BaseElement { export interface SelectorElement extends BaseElement { type: "Selector"; - onShow: () => Promise; - displayValue: (data: unknown | null) => string; + onShow: () => Promise; + displayValue: (data: any | null) => string; } const BaseComponent: React.FunctionComponent<{ @@ -79,8 +79,8 @@ const BaseComponent: React.FunctionComponent<{ export const ScaffoldedForm: React.FunctionComponent<{ element: ScaffoldedFormElement; - data: unknown | null; - onUpdate: (newData: unknown) => void; + data: any | null; + onUpdate: (newData: any) => void; ancestorId?: string; errors: React.RefObject>; }> = ({ancestorId, element, data, onUpdate, errors}) => { diff --git a/frontend-web/webclient/app/ui-components/Sidebar.tsx b/frontend-web/webclient/app/ui-components/Sidebar.tsx index 3328a578b1..3a6c0fc15d 100644 --- a/frontend-web/webclient/app/ui-components/Sidebar.tsx +++ b/frontend-web/webclient/app/ui-components/Sidebar.tsx @@ -45,7 +45,6 @@ import JobsApi, {Job} from "@/UCloud/JobsApi"; import {classConcat, injectStyle, injectStyleSimple} from "@/Unstyled"; import Relative from "./Relative"; import {SafeLogo} from "@/Applications/AppToolLogo"; -import {setAppFavorites} from "@/Applications/Redux/Actions"; import {checkCanConsumeResources} from "./ResourceBrowser"; import {api as FilesApi} from "@/UCloud/FilesApi"; import {getCssPropertyValue} from "@/Utilities/StylingUtilities"; @@ -67,7 +66,7 @@ import {ApplicationSummaryWithFavorite} from "@/Applications/AppStoreApi"; import {isAdminOrPI} from "@/Project"; import {FileType} from "@/Files"; import {onProjectUpdated, projectCache, projectTitle} from "@/Project/ProjectSwitcher"; -import {GenericSetAction, HookStore, useGlobal} from "@/Utilities/ReduxHooks"; +import {genericSet, HookStore, useGlobal} from "@/Utilities/ReduxHooks"; import {useDiscovery} from "@/Applications/Hooks"; import {Command, CommandPalette, CommandScope, staticProvider, useProvideCommands} from "@/CommandPalette"; import {NavigateFunction, useNavigate} from "react-router-dom"; @@ -76,6 +75,7 @@ import {Dispatch} from "redux"; import {AutomaticBranding} from "@/Applications/Branding/AutomaticBranding"; import {BrandingResponse} from "@/UCloud/BrandingApi"; import {Feature, hasFeature} from "@/Features"; +import {setAppFavorites} from "@/Applications/Redux/Reducer"; const SecondarySidebarClass = injectStyle("secondary-sidebar", k => ` ${k} { @@ -204,7 +204,7 @@ interface SidebarElement { } function SidebarTab({icon}: SidebarElement): React.ReactNode { - return + return } interface MenuElement { @@ -224,31 +224,31 @@ const sideBarMenuElements: [ SidebarMenuElements, SidebarMenuElements, ] = [ - { - items: [ - {icon: "heroFolder", label: SidebarTabId.FILES, to: AppRoutes.files.drives()}, - {icon: "heroUserGroup", label: SidebarTabId.PROJECT, to: AppRoutes.project.allocations()}, - {icon: "heroSquaresPlus", label: SidebarTabId.RESOURCES, to: AppRoutes.resources.publicLinks()}, - {icon: "heroShoppingBag", label: SidebarTabId.APPLICATIONS, to: AppRoutes.apps.landing()}, - {icon: "heroServer", label: SidebarTabId.RUNS, to: AppRoutes.compute.jobs()} - ], - predicate: () => Client.isLoggedIn - }, - { - items: [ - {icon: "heroBolt", label: SidebarTabId.ADMIN, to: AppRoutes.admin.userCreation()}, - ], - predicate: () => Client.userIsAdmin - }, - { - items: [ - {icon: "heroBuildingStorefront", label: SidebarTabId.APPLICATION_STUDIO, to: AppRoutes.appStudio.groups()} - ], - predicate: (state) => { - return Client.userIsAdmin; + { + items: [ + {icon: "heroFolder", label: SidebarTabId.FILES, to: AppRoutes.files.drives()}, + {icon: "heroUserGroup", label: SidebarTabId.PROJECT, to: AppRoutes.project.allocations()}, + {icon: "heroSquaresPlus", label: SidebarTabId.RESOURCES, to: AppRoutes.resources.publicLinks()}, + {icon: "heroShoppingBag", label: SidebarTabId.APPLICATIONS, to: AppRoutes.apps.landing()}, + {icon: "heroServer", label: SidebarTabId.RUNS, to: AppRoutes.compute.jobs()} + ], + predicate: () => Client.isLoggedIn + }, + { + items: [ + {icon: "heroBolt", label: SidebarTabId.ADMIN, to: AppRoutes.admin.userCreation()}, + ], + predicate: () => Client.userIsAdmin + }, + { + items: [ + {icon: "heroBuildingStorefront", label: SidebarTabId.APPLICATION_STUDIO, to: AppRoutes.appStudio.groups()} + ], + predicate: (state) => { + return Client.userIsAdmin; + } } - } -]; + ]; interface SidebarStateProps { loggedIn: boolean; @@ -279,11 +279,11 @@ const SidebarItemsClass = injectStyle("sidebar-items", k => ` } `); -function UserMenuLink(props: { icon: IconName; text: string; to: string; close(): void; }): React.ReactNode { +function UserMenuLink(props: {icon: IconName; text: string; to: string; close(): void;}): React.ReactNode { return + size="1.3em" /> {props.text} @@ -298,7 +298,7 @@ function UserMenuExternalLink(props: { if (!props.text) return null; return
- + {props.text}
@@ -336,7 +336,7 @@ function UserMenu({branding, avatar, dialog, setOpenDialog}: { closeFnRef={close} colorOnHover={false} trigger={Client.isLoggedIn ? - : null} + : null} > {branding.statusPage ? ( @@ -349,18 +349,18 @@ function UserMenu({branding, avatar, dialog, setOpenDialog}: {
- + - ): null } + ) : null} - { branding.documentation ? + {branding.documentation ? () - : null } - { branding.dataProtection ? + : null} + {branding.dataProtection ? () : null + text={branding.dataProtection.title} />) : null } @@ -368,7 +368,7 @@ function UserMenu({branding, avatar, dialog, setOpenDialog}: { Client.logout()} data-component={"logout-button"}> - + Logout
@@ -381,7 +381,7 @@ function CommandPaletteEntry(): React.ReactNode { return { window.dispatchEvent(new KeyboardEvent("keydown", {code: "KeyP", ctrlKey: true, metaKey: true})); }}> - Command palette + Command palette ({CTRL_KEY} + P) } @@ -460,10 +460,11 @@ export function Sidebar(): React.ReactNode { const [selectedPage, setSelectedPage] = React.useState(SidebarTabId.NONE); const [hoveredPage, setHoveredPage] = React.useState(SidebarTabId.NONE); + const dispatch = useDispatch(); + const tab = useSelector((it: ReduxObject) => it.status.tab); const branding = useSelector((it: ReduxObject) => it.branding); - const dispatch = useDispatch(); React.useEffect(() => { if (Client.isLoggedIn) { findAvatar().then(action => { @@ -493,8 +494,8 @@ export function Sidebar(): React.ReactNode {
- + onClick={onLogoClick}> +
to ? ( + key={label} to={typeof to === "function" ? to() : to}>
setHoveredPage(label)} @@ -519,20 +520,20 @@ export function Sidebar(): React.ReactNode { }} className={SidebarMenuItem} > - +
) :
{ - if (selectedPage) { - setSelectedPage(label); - } - }} - onMouseEnter={() => setHoveredPage(label)} - className={SidebarMenuItem} - > - + key={label} + data-active={tab === label} + onClick={() => { + if (selectedPage) { + setSelectedPage(label); + } + }} + onMouseEnter={() => setHoveredPage(label)} + className={SidebarMenuItem} + > +
)}
@@ -620,7 +621,7 @@ function useSidebarFilesPage(): [ try { const f = await callAPI(FilesApi.retrieve({id: file.path})) fileTypeCache[file.path] = f.status.type; - } catch (e) { + } catch (e: any) { if (e?.request?.status === 404) { fileTypeCache[file.path] = "DELETED"; callAPI( @@ -769,7 +770,7 @@ function ProjectSubLinks({canApply, isPersonalWorkspace, projectId}: { projectId?: string }) { const sublinks = React.useMemo(() => - projectSidebarSubLinks(canApply, isPersonalWorkspace, projectId).filter(it => !it.disabled), + projectSidebarSubLinks(canApply, isPersonalWorkspace, projectId).filter(it => !it.disabled), [canApply, isPersonalWorkspace, projectId]); return sublinks.map(it => ); } @@ -822,13 +823,13 @@ function projectSidebarSubLinks(canApply: boolean, isPersonalWorkspace: boolean, }, { to: outgoing(), text: "Grant applications", icon: "heroDocumentText", tab, defaultHidden: true, }, - { - to: !canApply || isPersonalWorkspace ? AppRoutes.grants.editor() : AppRoutes.grants.newApplication({projectId: projectId}), - text: "Apply for resources", - icon: "heroPencilSquare", - disabled: !canApply, - tab, - }]; + { + to: !canApply || isPersonalWorkspace ? AppRoutes.grants.editor() : AppRoutes.grants.newApplication({projectId: projectId}), + text: "Apply for resources", + icon: "heroPencilSquare", + disabled: !canApply, + tab, + }]; } @@ -836,22 +837,22 @@ const ApplicationStudioSubLinksEntries: LinkInfo[] = [{ to: AppRoutes.appStudio.groups(), text: "Applications", icon: "heroSquare3Stack3D", tab: SidebarTabId.APPLICATION_STUDIO }, - { - to: AppRoutes.appStudio.categories(), text: "Categories", icon: "heroSquaresPlus", - tab: SidebarTabId.APPLICATION_STUDIO - }, - { - to: AppRoutes.appStudio.hero(), text: "Carrousel", icon: "heroFilm", - tab: SidebarTabId.APPLICATION_STUDIO - }, - { - to: AppRoutes.appStudio.topPicks(), text: "Top picks", icon: "heroTrophy", - tab: SidebarTabId.APPLICATION_STUDIO - }, - { - to: AppRoutes.appStudio.spotlights(), text: "Spotlights", icon: "heroCamera", - tab: SidebarTabId.APPLICATION_STUDIO - }]; +{ + to: AppRoutes.appStudio.categories(), text: "Categories", icon: "heroSquaresPlus", + tab: SidebarTabId.APPLICATION_STUDIO +}, +{ + to: AppRoutes.appStudio.hero(), text: "Carrousel", icon: "heroFilm", + tab: SidebarTabId.APPLICATION_STUDIO +}, +{ + to: AppRoutes.appStudio.topPicks(), text: "Top picks", icon: "heroTrophy", + tab: SidebarTabId.APPLICATION_STUDIO +}, +{ + to: AppRoutes.appStudio.spotlights(), text: "Spotlights", icon: "heroCamera", + tab: SidebarTabId.APPLICATION_STUDIO +}]; function ApplicationStudioSubLinks() { const isAdmin = Client.userIsAdmin; @@ -861,13 +862,13 @@ function ApplicationStudioSubLinks() { } function SecondarySidebar({ - hovered, - clicked, - setHoveredPage, - clearHover, - setSelectedPage, - clearClicked - }: SecondarySidebarProps): React.ReactNode { + hovered, + clicked, + setHoveredPage, + clearHover, + setSelectedPage, + clearClicked +}: SecondarySidebarProps): React.ReactNode { const [drives, favoriteFiles] = useSidebarFilesPage(); const recentRuns = useSidebarRunsPage(); const projectId = useProjectId(); @@ -1022,17 +1023,12 @@ function SecondarySidebar({ document.body.style.setProperty(CSSVarCurrentSidebarWidth, `${sum}px`); document.body.style.setProperty(CSSVarCurrentSidebarStickyWidth, isOpen && !asPopOver ? `${sum}px` : `${firstLevel}px`); - dispatch({ - type: "GENERIC_SET", - payload: {property: "sidebarWidth", newValue: sum, defaultValue: sum} - }); - dispatch({ - type: "GENERIC_SET", payload: { - property: "sidebarStickyWidth", - newValue: isOpen && !asPopOver ? sum : firstLevel, - defaultValue: isOpen && !asPopOver ? sum : firstLevel, - } - }); + dispatch(genericSet({property: "sidebarWidth", newValue: sum, defaultValue: sum})); + dispatch(genericSet({ + property: "sidebarStickyWidth", + newValue: isOpen && !asPopOver ? sum : firstLevel, + defaultValue: isOpen && !asPopOver ? sum : firstLevel, + })); }, [isOpen, asPopOver]); const onMenuClick = useCallback((ev: React.SyntheticEvent) => { @@ -1073,10 +1069,10 @@ function SecondarySidebar({ setSelectedPage(hovered)}> - + height="38px" width={"30px"} + justifyContent={"center"} borderRadius="12px 0 0 12px" + onClick={clicked ? onClear : () => setSelectedPage(hovered)}> + @@ -1084,7 +1080,7 @@ function SecondarySidebar({ {active !== SidebarTabId.FILES ? null : <> Drives + tab={SidebarTabId.FILES}>Drives {(!canConsume || drives.data.items.length === 0) && <> No drives available } @@ -1094,8 +1090,8 @@ function SecondarySidebar({ key={drive.id} text={drive.specification.title} icon={isShare(drive) ? - : - } + : + } to={AppRoutes.files.drive(drive.id)} tab={SidebarTabId.FILES} /> @@ -1117,18 +1113,18 @@ function SecondarySidebar({ {canConsume && sharesLinksInfo.length > 0 && isPersonalWorkspace ? <> Shared files - + : null} } {active !== SidebarTabId.PROJECT ? null : <> - - + + } {active !== SidebarTabId.RESOURCES ? null : <> - - + + } {/* Note(Jonas) Do it this way to ensure that the frontend doesn't fetch icons every time this is shown. */} @@ -1140,7 +1136,7 @@ function SecondarySidebar({ key={fav.metadata.name} to={AppRoutes.jobs.create(fav.metadata.name)} text={fav.metadata.title} - icon={} + icon={} tab={SidebarTabId.APPLICATIONS} /> )} @@ -1167,7 +1163,7 @@ function SecondarySidebar({ {/* Note(Jonas) Do it this way to ensure that the frontend doesn't fetch icons every time this is shown. */}
- + Running jobs {recentRuns.length === 0 && <> @@ -1184,7 +1180,7 @@ function SecondarySidebar({ key={run.id} to={AppRoutes.jobs.view(run.id)} text={name} - icon={} + icon={} tab={SidebarTabId.RUNS} /> })} @@ -1193,30 +1189,30 @@ function SecondarySidebar({ {active !== SidebarTabId.ADMIN ? null : <> Tools + tab={SidebarTabId.ADMIN} /> + tab={SidebarTabId.ADMIN} /> + tab={SidebarTabId.ADMIN} /> + tab={SidebarTabId.ADMIN} /> } {active !== SidebarTabId.APPLICATION_STUDIO ? null : <> - - + + }
; } -function AppLogo({name}: { name: string }): React.ReactNode { - return ; +function AppLogo({name}: {name: string}): React.ReactNode { + return ; } function SidebarSectionEmptyHeader(): React.ReactNode { - return + return } function Username(): React.ReactNode { @@ -1249,7 +1245,7 @@ function Username(): React.ReactNode { )} > - This is your username.

+ This is your username.

Click to copy to clipboard. } @@ -1297,7 +1293,7 @@ function ProjectID(): React.ReactNode { } > - This is your project ID.

+ This is your project ID.

Click to copy to clipboard. } @@ -1325,8 +1321,8 @@ function Downtimes(): React.ReactNode { if (upcomingDowntime === -1) return null; return - }> - Upcoming downtime.
+ }> + Upcoming downtime.
Click to view
diff --git a/frontend-web/webclient/app/ui-components/TemporalLineChart.tsx b/frontend-web/webclient/app/ui-components/TemporalLineChart.tsx index cb635bded8..5a55085176 100644 --- a/frontend-web/webclient/app/ui-components/TemporalLineChart.tsx +++ b/frontend-web/webclient/app/ui-components/TemporalLineChart.tsx @@ -2,7 +2,7 @@ import {DependencyList, useEffect, useId, useRef} from "react"; import {pointer, select} from "d3-selection"; import {Selection, timeDay, timeHour, timeMinute, timeMonth, timeSecond, timeWeek, timeYear} from "d3"; import {bisector, extent} from "d3-array"; -import {scaleLinear, scaleTime} from "d3-scale"; +import {NumberValue, scaleLinear, scaleTime} from "d3-scale"; import {axisBottom, axisLeft} from "d3-axis"; import {timeFormat} from "d3-time-format"; import {line} from "d3-shape"; @@ -18,13 +18,14 @@ const formatMillisecond = timeFormat(".%L"), formatMonth = timeFormat("%B"), formatYear = timeFormat("%Y"); -function multiFormat(date: Date) { +function multiFormat(_date: Date | NumberValue) { + const date = _date as Date; return (timeSecond(date) < date ? formatMillisecond : timeMinute(date) < date ? formatSecond - : timeHour(date) < date ? formatMinute - : timeDay(date) < date ? formatHour - : timeMonth(date) < date ? (timeWeek(date) < date ? formatDay : formatWeek) - : timeYear(date) < date ? formatMonth + : timeHour(date) < date ? formatMinute + : timeDay(date) < date ? formatHour + : timeMonth(date) < date ? (timeWeek(date) < date ? formatDay : formatWeek) + : timeYear(date) < date ? formatMonth : formatYear)(date); } @@ -236,7 +237,7 @@ export function TemporalLineChart( .append("tspan") .attr("x", 10) .attr("dy", 15) - .text(d => d.data.length > 0 ?yTickFormatter(last(d.data).v, false) : ""); + .text(d => d.data.length > 0 ? yTickFormatter(last(d.data).v, false) : ""); // Sort labels and deal with overlap // ------------------------------------------------------------------------------------------------------------- @@ -262,7 +263,7 @@ export function TemporalLineChart( const lastPt = last(d.data); const anchorY = yScale(lastPt.v); const bbox = (txt as SVGTextElement).getBBox(); // local to group - return { txt, g, anchorY, boxY: bbox.y, height: bbox.height }; + return {txt, g, anchorY, boxY: bbox.y, height: bbox.height}; }).filter(Boolean); if (!items.length) return; @@ -332,7 +333,7 @@ export function TemporalLineChart( .attr("y2", innerH) .attr("stroke", "currentColor") .attr("stroke-opacity", 0.35) - ; + ; const marker = tooltip.append("g"); @@ -415,5 +416,5 @@ export function TemporalLineChart( }; }, [lines, width, height, margin, liveDomainMs, yDomain]); - return ; + return ; } diff --git a/frontend-web/webclient/app/ui-components/ThemeToggle.tsx b/frontend-web/webclient/app/ui-components/ThemeToggle.tsx index 513bf0537b..3b63ceede5 100644 --- a/frontend-web/webclient/app/ui-components/ThemeToggle.tsx +++ b/frontend-web/webclient/app/ui-components/ThemeToggle.tsx @@ -1,10 +1,10 @@ import * as React from "react"; import {useDispatch} from "react-redux"; -import {toggleThemeRedux} from "@/Applications/Redux/Actions"; import {isLightThemeStored} from "@/UtilityFunctions"; import {toggleTheme} from "./theme"; import Icon from "./Icon"; import {injectStyle} from "@/Unstyled"; +import {toggleThemeRedux} from "@/Applications/Redux/Reducer"; export function ThemeToggler(): React.ReactNode { const isLightTheme = isLightThemeStored(); @@ -20,8 +20,8 @@ export function ThemeToggler(): React.ReactNode { return ( ); } diff --git a/frontend-web/webclient/package-lock.json b/frontend-web/webclient/package-lock.json index eb142e936a..1c373941ba 100644 --- a/frontend-web/webclient/package-lock.json +++ b/frontend-web/webclient/package-lock.json @@ -23,22 +23,22 @@ "date-fns": "4.1.0", "deepcopy": "2.1.0", "fast-deep-equal": "3.1.3", - "fuse.js": "7.1.0", + "fuse.js": "7.3.0", "immer": "10.2.0", - "jsrsasign": "11.1.1", + "jsrsasign": "11.1.3", "localforage": "1.10.0", "magic-bytes.js": "1.13.0", "mermaid": "11.14.0", "monaco-editor": "0.55.1", "monaco-vim": "0.4.2", - "react": "19.2.4", + "react": "19.2.5", "react-datepicker": "9.0.0", - "react-dom": "19.2.4", + "react-dom": "19.2.5", "react-markdown": "10.1.0", "react-modal": "3.16.3", "react-redux": "9.2.0", - "react-router": "7.13.0", - "react-router-dom": "7.13.0", + "react-router": "7.14.2", + "react-router-dom": "7.14.2", "react-syntax-highlighter": "16.1.0", "react-transition-group": "4.4.5", "react-virtualized-auto-sizer": "1.0.26", @@ -50,10 +50,10 @@ "ua-parser-js": "1.0.41", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", - "yaml": "2.8.3" + "yaml": "2.8.4" }, "devDependencies": { - "@playwright/test": "1.58.2", + "@playwright/test": "1.59.1", "@types/d3": "7.4.3", "@types/history": "5.0.0", "@types/json-schema": "7.0.15", @@ -69,7 +69,7 @@ "@types/ua-parser-js": "0.7.39", "@vitejs/plugin-react": "5.1.4", "@vitejs/plugin-react-refresh": "1.3.6", - "typescript": "5.9.3", + "typescript": "6.0.3", "vite": "7.3.2" } }, @@ -982,13 +982,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -3593,10 +3593,16 @@ } }, "node_modules/fuse.js": { - "version": "7.1.0", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", "license": "Apache-2.0", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" } }, "node_modules/generator-function": { @@ -4207,13 +4213,10 @@ } }, "node_modules/jsrsasign": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.1.tgz", - "integrity": "sha512-6w95OOXH8DNeGxakqLndBEqqwQ6A70zGaky1oxfg8WVLWOnghTfJsc5Tknx+Z88MHSb1bGLcqQHImOF8Lk22XA==", - "license": "MIT", - "funding": { - "url": "https://github.com/kjur/jsrsasign#donations" - } + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.3.tgz", + "integrity": "sha512-nPnK5D/4lv0Dwr7TlzrKtAd8JlLZwFTqTUUB3NQCbtdobcRcohGFxjbPySDVh74iWUudcCsapYT6OxoyhJLhhA==", + "license": "MIT" }, "node_modules/katex": { "version": "0.16.25", @@ -6683,13 +6686,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -6702,9 +6705,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6807,9 +6810,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6837,15 +6840,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.5" } }, "node_modules/react-is": { @@ -7041,9 +7044,9 @@ } }, "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -7063,12 +7066,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", - "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", "license": "MIT", "dependencies": { - "react-router": "7.13.0" + "react-router": "7.14.2" }, "engines": { "node": ">=20.0.0" @@ -7990,7 +7993,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -8434,9 +8439,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/frontend-web/webclient/package.json b/frontend-web/webclient/package.json index 8dcb38e31b..37c6db47b2 100644 --- a/frontend-web/webclient/package.json +++ b/frontend-web/webclient/package.json @@ -83,7 +83,7 @@ "@types/ua-parser-js": "0.7.39", "@vitejs/plugin-react": "5.1.4", "@vitejs/plugin-react-refresh": "1.3.6", - "typescript": "5.9.3", + "typescript": "6.0.3", "vite": "7.3.2" } } \ No newline at end of file diff --git a/frontend-web/webclient/tsconfig.json b/frontend-web/webclient/tsconfig.json index 879c7efcd4..0df0089910 100644 --- a/frontend-web/webclient/tsconfig.json +++ b/frontend-web/webclient/tsconfig.json @@ -4,29 +4,27 @@ "outDir": "./dist/", "alwaysStrict": true, "strictNullChecks": true, + "strictPropertyInitialization": false, "noImplicitThis": true, "noImplicitAny": false, "allowSyntheticDefaultImports": true, "allowUnreachableCode": false, "noFallthroughCasesInSwitch": true, "allowUnusedLabels": false, - "module": "esnext", "lib": [ "ESNext", "dom" ], "skipLibCheck": true, - "target": "es2015", + "target": "es2020", "jsx": "react", "pretty": true, "allowJs": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "sourceMap": true, - "baseUrl": ".", "types": [ "vite/client", - "monaco-editor/monaco", ], "paths": { "@/*": [ @@ -36,6 +34,8 @@ }, "exclude": [ "./__tests__/*", - "./dist/*" + "./dist/*", + "./playwright.config.ts", + "./tests/*" ] } \ No newline at end of file From f08567eaf08466c3f6d11ae4a482b3fedaff69cd Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 13 May 2026 13:22:11 +0200 Subject: [PATCH 04/19] Previous commit merged another branch and fixed merged conflicts. Text is wrong From 3e3e59cefdf05065febdb6c8858551a5f35cc1e6 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 18 May 2026 12:09:16 +0200 Subject: [PATCH 05/19] Fixes bug in sidebarReducer --- frontend-web/webclient/app/Applications/Redux/Reducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-web/webclient/app/Applications/Redux/Reducer.ts b/frontend-web/webclient/app/Applications/Redux/Reducer.ts index af48be57e8..f88ffea7d3 100644 --- a/frontend-web/webclient/app/Applications/Redux/Reducer.ts +++ b/frontend-web/webclient/app/Applications/Redux/Reducer.ts @@ -32,7 +32,7 @@ const sidebarSlice = createSlice({ } else { copy.favorites = state.favorites.filter(it => it.metadata.name !== metadata.metadata.name || it.metadata.version !== metadata.metadata.version); } - state = copy; + state.favorites = copy.favorites; }, toggleThemeRedux(state, action: PayloadAction<"light" | "dark">) { state.theme = action.payload; From 5b365f80b6e60cf4223819f04e3ebae8f13bd780 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 18 May 2026 12:20:23 +0200 Subject: [PATCH 06/19] Remove Dashboard reducer, seemingly not in use --- .../webclient/app/Dashboard/Dashboard.tsx | 14 -------------- .../webclient/app/Dashboard/Redux/index.tsx | 17 ----------------- frontend-web/webclient/app/DefaultObjects.ts | 2 -- .../webclient/app/Utilities/ReduxUtilities.tsx | 4 +--- frontend-web/webclient/tsconfig.json | 3 ++- 5 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 frontend-web/webclient/app/Dashboard/Redux/index.tsx diff --git a/frontend-web/webclient/app/Dashboard/Dashboard.tsx b/frontend-web/webclient/app/Dashboard/Dashboard.tsx index 0029924041..f860f19551 100644 --- a/frontend-web/webclient/app/Dashboard/Dashboard.tsx +++ b/frontend-web/webclient/app/Dashboard/Dashboard.tsx @@ -1,12 +1,8 @@ import {MainContainer} from "@/ui-components/MainContainer"; import {usePage} from "@/Navigation/Redux"; import * as React from "react"; -import {useDispatch} from "react-redux"; -import {Dispatch} from "redux"; import {Box, Button, Flex, Icon, Image, Link, Markdown, Text} from "@/ui-components"; import * as Heading from "@/ui-components/Heading"; -import {DashboardOperations} from "."; -import {setAllLoading} from "./Redux"; import {APICallState, useCloudAPI} from "@/Authentication/DataHook"; import {buildQueryString} from "@/Utilities/URIUtilities"; import {Spacer} from "@/ui-components/Spacer"; @@ -63,14 +59,11 @@ function Dashboard(): React.ReactNode { usePage("Dashboard", SidebarTabId.NONE); - const dispatch = useDispatch(); const invitesReload = React.useRef<() => void>(initialCall); const projectInvitesReload = React.useRef<() => void>(initialCall); const runsReload = React.useRef<() => void>(initialCall); const grantsReload = React.useRef<() => void>(initialCall); - const reduxOps = React.useMemo(() => reduxOperations(dispatch), [dispatch]); - const [wallets, fetchWallets] = useCloudAPI>({noop: true}, emptyPageV2); React.useEffect(() => { @@ -78,7 +71,6 @@ function Dashboard(): React.ReactNode { }, []); function reload(): void { - reduxOps.setAllLoading(true); fetchNews(newsParams); fetchWallets(Accounting.browseWalletsV2({ itemsPerPage: 250, @@ -472,12 +464,6 @@ const NewsClass = injectStyle("with-graphic", k => ` } `); -function reduxOperations(dispatch: Dispatch): DashboardOperations { - return { - setAllLoading: loading => dispatch(setAllLoading(loading)), - }; -} - const DashboardCard: React.FunctionComponent<{ title: string; linkTo?: string; diff --git a/frontend-web/webclient/app/Dashboard/Redux/index.tsx b/frontend-web/webclient/app/Dashboard/Redux/index.tsx deleted file mode 100644 index e04eb63028..0000000000 --- a/frontend-web/webclient/app/Dashboard/Redux/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {createSlice, PayloadAction} from '@reduxjs/toolkit' - -export const dashboardSlice = createSlice({ - name: "dashboard", - initialState: { - loading: false, - }, - reducers: { - setAllLoading: (state, action: PayloadAction) => { - state.loading = action.payload; - } - }, -}) - -export const {setAllLoading} = dashboardSlice.actions - -export const dashboardReducer = dashboardSlice.reducer; \ No newline at end of file diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index eb03677323..7d1119621e 100644 --- a/frontend-web/webclient/app/DefaultObjects.ts +++ b/frontend-web/webclient/app/DefaultObjects.ts @@ -23,7 +23,6 @@ export interface StatusReduxObject { */ export interface LegacyReduxObject { hookStore: HookStore; - dashboard: DashboardStateProps; status: StatusReduxObject; avatar: AvatarReduxObject; project: ProjectRedux.State; @@ -57,7 +56,6 @@ export function initDashboard(): DashboardStateProps { export function initObject(): ReduxObject { return { hookStore: {}, - dashboard: initDashboard(), status: initStatus(), avatar: initAvatar(), project: ProjectRedux.initialState, diff --git a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx index 414d8d8174..7303f252fe 100644 --- a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx @@ -1,7 +1,6 @@ import {useEffect} from "react"; -import {combineReducers, Reducer} from "redux"; +import {combineReducers} from "redux"; -import {dashboardReducer} from "@/Dashboard/Redux"; import {initObject} from "@/DefaultObjects"; import {statusReducer} from "@/Navigation/Redux"; import * as ProjectRedux from "@/Project/ReduxState"; @@ -41,7 +40,6 @@ function confStore( export const store = confStore(initObject(), { - dashboard: dashboardReducer, status: statusReducer, hookStore: hookStoreReducer, sidebar: sidebarReducer, diff --git a/frontend-web/webclient/tsconfig.json b/frontend-web/webclient/tsconfig.json index 0df0089910..c5dba279c9 100644 --- a/frontend-web/webclient/tsconfig.json +++ b/frontend-web/webclient/tsconfig.json @@ -13,7 +13,8 @@ "allowUnusedLabels": false, "lib": [ "ESNext", - "dom" + "dom", + "ES2023" ], "skipLibCheck": true, "target": "es2020", From 611866950dc7cf205fc70c1569f8a4fa7ac1cfd3 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 18 May 2026 12:40:31 +0200 Subject: [PATCH 07/19] Use terminal actions --- .../webclient/app/Terminal/Container.tsx | 14 ++++++------- .../webclient/app/UCloud/FilesApi.tsx | 20 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend-web/webclient/app/Terminal/Container.tsx b/frontend-web/webclient/app/Terminal/Container.tsx index 0e74e360b9..305bb04337 100644 --- a/frontend-web/webclient/app/Terminal/Container.tsx +++ b/frontend-web/webclient/app/Terminal/Container.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import {terminalClose, terminalCloseTab, terminalSelectTab, TerminalState, TerminalTab, useTerminalState} from "@/Terminal/State"; +import {terminalClose, terminalCloseTab, terminalOpen, terminalSelectTab, TerminalState, TerminalTab, useTerminalState} from "@/Terminal/State"; import {useCallback, useEffect, useRef, useMemo, useState} from "react"; import {Icon, Truncate} from "@/ui-components"; import {injectStyle} from "@/Unstyled"; @@ -14,7 +14,7 @@ import {CSSVarCurrentSidebarStickyWidth} from "@/ui-components/List"; import {Tab} from "@/Editor/Editor"; import {Operation, Operations, ShortcutKey} from "@/ui-components/Operation"; import {useDispatch} from "react-redux"; -import {Dispatch, PayloadAction} from "@reduxjs/toolkit"; +import {Dispatch} from "@reduxjs/toolkit"; const Wrapper = injectStyle("wrapper", k => ` ${k} { @@ -123,15 +123,15 @@ export const TerminalContainer: React.FunctionComponent = () => { const toggle = useCallback(() => { if (state.open) { - dispatch({type: "TerminalClose"}); + dispatch(terminalClose()); } else { - dispatch({type: "TerminalOpen"}); + dispatch(terminalOpen()); } }, [state.open]); const closeTerminal = useCallback((idx: number) => { if (state.activeTab >= 0) { - dispatch({type: "TerminalCloseTab", payload: {tabIdx: idx}}); + dispatch(terminalCloseTab({tabIdx: idx})) } }, [state.activeTab]); @@ -146,7 +146,7 @@ export const TerminalContainer: React.FunctionComponent = () => { {tab.title}} - onRowClick={() => dispatch({type: "TerminalSelectTab", payload: {tabIdx: idx}})} + onRowClick={() => dispatch(terminalSelectTab({tabIdx: idx}))} isActive={idx === state.activeTab} icon={
} onClose={e => { @@ -228,7 +228,7 @@ function tabOperations(dispatch: Dispatch, tabIdx: number, state: TerminalState) { text: "Close all", enabled: () => true, onClick() { for (let i = state.tabs.length - 1; i >= 0; i--) { - dispatch({type: "TerminalCloseTab", payload: {tabIdx: i}}); + dispatch(terminalCloseTab({tabIdx: i})); } dispatch(terminalClose()); }, diff --git a/frontend-web/webclient/app/UCloud/FilesApi.tsx b/frontend-web/webclient/app/UCloud/FilesApi.tsx index a3c9d366ae..ceaeae5d1b 100644 --- a/frontend-web/webclient/app/UCloud/FilesApi.tsx +++ b/frontend-web/webclient/app/UCloud/FilesApi.tsx @@ -80,6 +80,8 @@ import {setPopInChild} from "@/ui-components/PopIn"; import {FileWriteFailure, WriteFailureEvent} from "@/Files/Uploader"; import {GuessedFile} from "magic-bytes.js/dist/model/tree"; import {sendFailureNotification, sendInformationNotification, sendSuccessNotification} from "@/Notifications"; +import {terminalOpen, terminalOpenTab} from "@/Terminal/State"; +import {genericSet} from "@/Utilities/ReduxHooks"; export function normalizeDownloadEndpoint(endpoint: string): string { const e = endpoint.replace("integration-module:8889", "localhost:8889"); @@ -283,12 +285,10 @@ class FilesApi extends ResourceApi { - cb.dispatch({ - type: "GENERIC_SET", payload: { - property: "uploaderVisible", newValue: true, - defaultValue: false - } - }); + cb.dispatch(genericSet({ + property: "uploaderVisible", newValue: true, + defaultValue: false + })); }, shortcut: ShortcutKey.U }, @@ -625,8 +625,8 @@ class FilesApi extends ResourceApi { From 525682ed6af9b30d573fb0d41cbbeba0d734f8fe Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 18 May 2026 13:31:52 +0200 Subject: [PATCH 08/19] Fix provider branding reducer Remove `loading` reducer --- frontend-web/webclient/app/DefaultObjects.ts | 2 -- frontend-web/webclient/app/Navigation/UtilityBar.tsx | 5 ++--- .../ProviderBrandings/AutomaticProviderBranding.tsx | 2 +- frontend-web/webclient/app/Utilities/ReduxHooks.ts | 2 +- .../webclient/app/Utilities/ReduxUtilities.tsx | 12 ------------ 5 files changed, 4 insertions(+), 19 deletions(-) diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index 7d1119621e..cc2aa47c5e 100644 --- a/frontend-web/webclient/app/DefaultObjects.ts +++ b/frontend-web/webclient/app/DefaultObjects.ts @@ -30,7 +30,6 @@ export interface LegacyReduxObject { providerBrandings: ProviderBrandingResponse; branding: BrandingResponse popinChild: PopInArgs; - loading: boolean; sidebar: SidebarStateProps; } @@ -63,7 +62,6 @@ export function initObject(): ReduxObject { providerBrandings: initProviderBranding(), branding: initBranding(), popinChild: {el: undefined}, - loading: false, sidebar: {favorites: [], theme: getThemeOrDefaultValue()} }; } diff --git a/frontend-web/webclient/app/Navigation/UtilityBar.tsx b/frontend-web/webclient/app/Navigation/UtilityBar.tsx index 8b7159195d..2716713cf8 100644 --- a/frontend-web/webclient/app/Navigation/UtilityBar.tsx +++ b/frontend-web/webclient/app/Navigation/UtilityBar.tsx @@ -104,9 +104,8 @@ const refreshIconClass = injectStyle("refresh-icon", k => ` function RefreshIcon(): React.ReactNode { const refresh = useRefresh(); - const spin = useSelector((it: ReduxObject) => it.loading); const loading = useSelector((it: ReduxObject) => it.status.loading); - return ; + return ; } export const RefreshButton: React.FunctionComponent<{ @@ -138,5 +137,5 @@ export const RefreshButton: React.FunctionComponent<{ if (refresh === noopCall) return null; return ; + id={"refresh-icon"} className={refreshIconClass} color="textPrimary" name="heroArrowPath" />; } \ No newline at end of file diff --git a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx index 6c162e6af1..6a41e44011 100644 --- a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx +++ b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx @@ -40,7 +40,7 @@ const providerBrandingSlice = createSlice({ initialState: initProviderBranding(), reducers: { addProviderBranding(state, action: PayloadAction) { - state = action.payload; + state.providers = action.payload.providers; } } }); diff --git a/frontend-web/webclient/app/Utilities/ReduxHooks.ts b/frontend-web/webclient/app/Utilities/ReduxHooks.ts index f9c70e0b40..19236c165a 100644 --- a/frontend-web/webclient/app/Utilities/ReduxHooks.ts +++ b/frontend-web/webclient/app/Utilities/ReduxHooks.ts @@ -54,7 +54,7 @@ const hookStore = createSlice({ initialState: initialState(), reducers: { genericSet(state, action: PayloadAction<{ - property: string; + property: keyof HookStore; newValue?: ValueOrSetter; defaultValue: any; }>) { diff --git a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx index 7303f252fe..7a2d68b4a0 100644 --- a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx @@ -47,22 +47,10 @@ export const store = confStore(initObject(), { terminal: terminalReducer, providerBrandings: providerBrandingReducer, branding: brandingReducer, - loading, project: ProjectRedux.reducer, popinChild: popInReducer, }); -function loading(state = false, action: {type: string}): boolean { - switch (action.type) { - case "LOADING_START": - return true; - case "LOADING_END": - return false; - default: - return state; - } -} - export const refreshFunctionCache = new class { private refresh: (() => void) | undefined = undefined; private subscribers: (() => void)[] = []; From 728fee6458ca5d057ede37e59a3baecb3629f977 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Mon, 18 May 2026 14:17:25 +0200 Subject: [PATCH 09/19] Fixes popin reducer Fixes contextMenu not working in filetree --- frontend-web/webclient/app/Files/FileTree.tsx | 1 + frontend-web/webclient/app/ui-components/PopIn.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend-web/webclient/app/Files/FileTree.tsx b/frontend-web/webclient/app/Files/FileTree.tsx index 381f37d84e..6338f5d93f 100644 --- a/frontend-web/webclient/app/Files/FileTree.tsx +++ b/frontend-web/webclient/app/Files/FileTree.tsx @@ -92,6 +92,7 @@ export function FileTree({tree, onTreeAction, onNodeActivated, root, ...props}: forceEvaluationOnOpen={true} openFnRef={openOperations} selected={[]} + row={42 as any} // This works, for some reason extra={null} hidden location={"IN_ROW"} diff --git a/frontend-web/webclient/app/ui-components/PopIn.tsx b/frontend-web/webclient/app/ui-components/PopIn.tsx index 15cdca8a83..cd2cd079d8 100644 --- a/frontend-web/webclient/app/ui-components/PopIn.tsx +++ b/frontend-web/webclient/app/ui-components/PopIn.tsx @@ -36,14 +36,15 @@ const PopInClass = injectStyle("popin-class", k => ` export function RightPopIn(): React.ReactNode { const content = useSelector(it => it.popinChild); + const dispatch = useDispatch(); /* Alternatively, use React.portal */ return setPopInChild({el: undefined})} />} + left={ dispatch(setPopInChild({el: undefined}))} />} right={content?.onFullScreen ? { content?.onFullScreen?.(); - setPopInChild({el: undefined}); + dispatch(setPopInChild({el: undefined})); }} /> : null} /> @@ -67,7 +68,9 @@ const popInSlice = createSlice({ initialState: initialState(), reducers: { setPopInChild(state, action: PayloadAction) { - state = action.payload; + state.el = action.payload.el; + state.onFullScreen = action.payload.onFullScreen; + } } }); From f8801d835b448b96d945e5d4e79c745cd5c823c2 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Tue, 19 May 2026 09:51:26 +0200 Subject: [PATCH 10/19] Revert HTML5FileSelector changes --- .../webclient/app/Files/HTML5FileSelector.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/frontend-web/webclient/app/Files/HTML5FileSelector.ts b/frontend-web/webclient/app/Files/HTML5FileSelector.ts index 4af601f559..03566a45d1 100644 --- a/frontend-web/webclient/app/Files/HTML5FileSelector.ts +++ b/frontend-web/webclient/app/Files/HTML5FileSelector.ts @@ -145,11 +145,11 @@ interface DropEventFolder { fileFetcher: () => Promise; } -export async function filesFromDropOrSelectEvent(event: React.DragEvent): Promise { +export async function filesFromDropOrSelectEvent(event): Promise { const dataTransfer = event.dataTransfer; if (!dataTransfer) { const files: PackagedFile[] = []; - const inputFieldFileList: File[] | undefined = event.target && event.target["files"]; + const inputFieldFileList = event.target && event.target.files; const fileList = inputFieldFileList || []; for (let i = 0; i < fileList.length; i++) { @@ -163,16 +163,12 @@ export async function filesFromDropOrSelectEvent(event: React.DragEvent): Promis } const entries: FileSystemEntry[] = []; - [...dataTransfer.items].forEach((listItem) => { - if (typeof listItem['webkitGetAsEntry'] === 'function') { - /* TODO(Jonas): The FileSystemEntry is now defined by TS-definitions, but they don't match with the file-local version */ - const entry: FileSystemEntry | null = listItem.webkitGetAsEntry() as unknown as FileSystemEntry; - if (entry) { - entries.push(entry); - } + [].slice.call(dataTransfer.items).forEach((listItem) => { + if (typeof listItem.webkitGetAsEntry === 'function') { + const entry: FileSystemEntry = listItem.webkitGetAsEntry(); + entries.push(entry); } else { - const theFile: File | null = listItem.getAsFile(); - if (!theFile) return; + const theFile: File = listItem.getAsFile(); const entry: FileSystemEntry = { filesystem: 1, From a2961fbbd3cb380ad27be494dad7d003d2504cf9 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Tue, 19 May 2026 10:03:41 +0200 Subject: [PATCH 11/19] Fix wrong usage of genericSet in ResourceBrowser --- frontend-web/webclient/app/Files/FileBrowse.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend-web/webclient/app/Files/FileBrowse.tsx b/frontend-web/webclient/app/Files/FileBrowse.tsx index e439130274..dac8e31000 100644 --- a/frontend-web/webclient/app/Files/FileBrowse.tsx +++ b/frontend-web/webclient/app/Files/FileBrowse.tsx @@ -76,6 +76,7 @@ import {HTMLTooltip} from "@/ui-components/Tooltip"; import SharesApi, {OutgoingShareGroup} from "@/UCloud/SharesApi"; import {sendFailureNotification, sendSuccessNotification} from "@/Notifications"; import {ProductStorage} from "@/Accounting"; +import {genericSet} from "@/Utilities/ReduxHooks"; export enum SensitivityLevel { "INHERIT" = "Inherit", @@ -1240,19 +1241,19 @@ function FileBrowse({ if (openTriggeredByPath.current === newPath) { openTriggeredByPath.current = null; } else if (!isSelector) { - // Note(Jonas): Edge case that we want to navigate to FileTable on breadcrumb click (Job/View) + // Note(Jonas): Edge case that we want to navigate to FileTable on breadcrumb click (Job/View) if (!isInitialMount.current && (oldPath !== newPath || opts?.embedded)) { navigate("/files?path=" + encodeURIComponent(newPath)); } } if (!isSelector) { - dispatch({ - type: "GENERIC_SET", payload: { + dispatch( + genericSet({ property: "uploadPath", newValue: newPath, defaultValue: newPath - } - }); + }) + ); } const collectionId = pathComponents(newPath)[0]; From 1f3e253972d118f83eeba6d3923a5ce283f40a94 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Tue, 19 May 2026 10:30:56 +0200 Subject: [PATCH 12/19] String change, and revert "Revert HTML5FileSelector changes" This reverts commit f8801d835b448b96d945e5d4e79c745cd5c823c2. --- .../webclient/app/Files/HTML5FileSelector.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend-web/webclient/app/Files/HTML5FileSelector.ts b/frontend-web/webclient/app/Files/HTML5FileSelector.ts index 03566a45d1..4af601f559 100644 --- a/frontend-web/webclient/app/Files/HTML5FileSelector.ts +++ b/frontend-web/webclient/app/Files/HTML5FileSelector.ts @@ -145,11 +145,11 @@ interface DropEventFolder { fileFetcher: () => Promise; } -export async function filesFromDropOrSelectEvent(event): Promise { +export async function filesFromDropOrSelectEvent(event: React.DragEvent): Promise { const dataTransfer = event.dataTransfer; if (!dataTransfer) { const files: PackagedFile[] = []; - const inputFieldFileList = event.target && event.target.files; + const inputFieldFileList: File[] | undefined = event.target && event.target["files"]; const fileList = inputFieldFileList || []; for (let i = 0; i < fileList.length; i++) { @@ -163,12 +163,16 @@ export async function filesFromDropOrSelectEvent(event): Promise { } const entries: FileSystemEntry[] = []; - [].slice.call(dataTransfer.items).forEach((listItem) => { - if (typeof listItem.webkitGetAsEntry === 'function') { - const entry: FileSystemEntry = listItem.webkitGetAsEntry(); - entries.push(entry); + [...dataTransfer.items].forEach((listItem) => { + if (typeof listItem['webkitGetAsEntry'] === 'function') { + /* TODO(Jonas): The FileSystemEntry is now defined by TS-definitions, but they don't match with the file-local version */ + const entry: FileSystemEntry | null = listItem.webkitGetAsEntry() as unknown as FileSystemEntry; + if (entry) { + entries.push(entry); + } } else { - const theFile: File = listItem.getAsFile(); + const theFile: File | null = listItem.getAsFile(); + if (!theFile) return; const entry: FileSystemEntry = { filesystem: 1, From 2c567a0af8917a07ce2bf818bf3018d19555a049 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Tue, 19 May 2026 10:31:03 +0200 Subject: [PATCH 13/19] Add the string-change, please! --- frontend-web/webclient/app/UCloud/FilesApi.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-web/webclient/app/UCloud/FilesApi.tsx b/frontend-web/webclient/app/UCloud/FilesApi.tsx index ceaeae5d1b..6112d4d054 100644 --- a/frontend-web/webclient/app/UCloud/FilesApi.tsx +++ b/frontend-web/webclient/app/UCloud/FilesApi.tsx @@ -1317,7 +1317,7 @@ export function FilePreview({initialFile}: { const failedUpload = e.detail.find(it => it.targetPath + it.name === path); if (failedUpload) { revert(); - sendFailureNotification(failedUpload.error ?? "Upload for file " + fileName(failedUpload.name) + " failed."); + sendFailureNotification(failedUpload.error ?? `Upload for file ${fileName(failedUpload.name)} failed.`); } } From 8d7cde2e5fc8eba655b6611d8e59d292327fb7ec Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 20 May 2026 11:08:06 +0200 Subject: [PATCH 14/19] Apply suggestion from @hschu12 Co-authored-by: Henrik Schulz --- .../webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts b/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts index e602056eff..5f6e8eb7c1 100644 --- a/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts +++ b/frontend-web/webclient/app/Applications/Jobs/JobViz/StreamProcessor.ts @@ -46,6 +46,7 @@ export function useJobVizProperties(processor: StreamProcessor): Record { + // Note(Jonas): First arg is just to make the compiler stop complaining. It's not used. processor.removeListener("kvPropertiesUpdated", listener); }; }, [processor]); From 06c153445830a43011b9bd4e6cd7b1f7c3acb05a Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 20 May 2026 11:11:04 +0200 Subject: [PATCH 15/19] Indentation fixes --- .../Jobs/Resources/PrivateNetworks.tsx | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx b/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx index cc30b3c412..687efad6c7 100644 --- a/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx +++ b/frontend-web/webclient/app/Applications/Jobs/Resources/PrivateNetworks.tsx @@ -10,17 +10,7 @@ import {doNothing} from "@/UtilityFunctions"; import {peerResourceAllowed} from "@/Applications/Jobs/Resources/Peers"; import {Feature, hasFeature} from "@/Features"; -export const PrivateNetworkResource: React.FunctionComponent<{ - application: Application; - params: ApplicationParameter[]; - errors: Record; - setErrors: React.Dispatch>> - onAdd: () => void; - onRemove: (id: string) => void; - provider?: string; - dnsHostname: string; - onDnsHostnameChange: (ev: React.SyntheticEvent) => void; -}> = ({ +export function PrivateNetworkResource({ application, params, errors, @@ -30,73 +20,83 @@ export const PrivateNetworkResource: React.FunctionComponent<{ provider, dnsHostname, onDnsHostnameChange, -}) => { - if (!peerResourceAllowed(application) || !hasFeature(Feature.NEW_VM_UI)) return null; - - return ( - - - - - Connect to other jobs - - - +}: { + application: Application; + params: ApplicationParameter[]; + errors: Record; + setErrors: React.Dispatch>> + onAdd: () => void; + onRemove: (id: string) => void; + provider?: string; + dnsHostname: string; + onDnsHostnameChange: (ev: React.SyntheticEvent) => void; +}) { + if (!peerResourceAllowed(application) || !hasFeature(Feature.NEW_VM_UI)) return null; - - {params.length !== 0 ? ( - - -
- Your job will be identified by this name within the network. -
-
- ) : ( - <> - If you need to connect this job to a network of other jobs then click {" "} - { - e.preventDefault(); - onAdd(); - }} - > - "Connect network" - - {" "} - to select one. You can manage networks in {" "} - - private networks - - . - - )} + return ( + + + + + Connect to other jobs + + - {params.map(entry => ( - - { - onRemove(entry.name); - }} - /> + + {params.length !== 0 ? ( + + +
+ Your job will be identified by this name within the network. +
- ))} + ) : ( + <> + If you need to connect this job to a network of other jobs then click {" "} + { + e.preventDefault(); + onAdd(); + }} + > + "Connect network" + + {" "} + to select one. You can manage networks in {" "} + + private networks + + . + + )}
-
- ); - }; + + {params.map(entry => ( + + { + onRemove(entry.name); + }} + /> + + ))} +
+
+ ); +}; From b666251267d60c45cb0210ef5a2f13044d0e26f6 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Wed, 20 May 2026 13:00:03 +0200 Subject: [PATCH 16/19] Indent change --- .../webclient/app/ui-components/Sidebar.tsx | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend-web/webclient/app/ui-components/Sidebar.tsx b/frontend-web/webclient/app/ui-components/Sidebar.tsx index 1a0b750541..7ff9bf5d3d 100644 --- a/frontend-web/webclient/app/ui-components/Sidebar.tsx +++ b/frontend-web/webclient/app/ui-components/Sidebar.tsx @@ -223,32 +223,30 @@ const sideBarMenuElements: [ SidebarMenuElements, SidebarMenuElements, SidebarMenuElements, -] = [ - { - items: [ - {icon: "heroFolder", label: SidebarTabId.FILES, to: AppRoutes.files.drives()}, - {icon: "heroUserGroup", label: SidebarTabId.PROJECT, to: AppRoutes.project.allocations()}, - {icon: "heroSquaresPlus", label: SidebarTabId.RESOURCES, to: AppRoutes.resources.publicLinks()}, - {icon: "heroShoppingBag", label: SidebarTabId.APPLICATIONS, to: AppRoutes.apps.landing()}, - {icon: "heroServer", label: SidebarTabId.RUNS, to: AppRoutes.compute.jobs()} - ], - predicate: () => Client.isLoggedIn - }, - { - items: [ - {icon: "heroBolt", label: SidebarTabId.ADMIN, to: AppRoutes.admin.userCreation()}, - ], - predicate: () => Client.userIsAdmin - }, - { - items: [ - {icon: "heroBuildingStorefront", label: SidebarTabId.APPLICATION_STUDIO, to: AppRoutes.appStudio.groups()} - ], - predicate: (state) => { - return Client.userIsAdmin; - } - } - ]; +] = [{ + items: [ + {icon: "heroFolder", label: SidebarTabId.FILES, to: AppRoutes.files.drives()}, + {icon: "heroUserGroup", label: SidebarTabId.PROJECT, to: AppRoutes.project.allocations()}, + {icon: "heroSquaresPlus", label: SidebarTabId.RESOURCES, to: AppRoutes.resources.publicLinks()}, + {icon: "heroShoppingBag", label: SidebarTabId.APPLICATIONS, to: AppRoutes.apps.landing()}, + {icon: "heroServer", label: SidebarTabId.RUNS, to: AppRoutes.compute.jobs()} + ], + predicate: () => Client.isLoggedIn +}, +{ + items: [ + {icon: "heroBolt", label: SidebarTabId.ADMIN, to: AppRoutes.admin.userCreation()}, + ], + predicate: () => Client.userIsAdmin +}, +{ + items: [ + {icon: "heroBuildingStorefront", label: SidebarTabId.APPLICATION_STUDIO, to: AppRoutes.appStudio.groups()} + ], + predicate: (state) => { + return Client.userIsAdmin; + } +}]; interface SidebarStateProps { loggedIn: boolean; From bdaac0584c92db738f3e89cf20f86ce617c2ec03 Mon Sep 17 00:00:00 2001 From: Schulz Date: Wed, 20 May 2026 13:21:25 +0200 Subject: [PATCH 17/19] version bump before deploy to dev --- core2/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core2/version.txt b/core2/version.txt index b1ad526dc1..360533059e 100644 --- a/core2/version.txt +++ b/core2/version.txt @@ -1 +1 @@ -2026.3.1 +2026.3.1-es6 From 05b606bf1693cef7df3a048c1f2e9f9766d0b2a4 Mon Sep 17 00:00:00 2001 From: Jonas Malte Hinchely Date: Thu, 21 May 2026 13:58:00 +0200 Subject: [PATCH 18/19] Add module property for our dynamic imports in Monaco editor --- frontend-web/webclient/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend-web/webclient/tsconfig.json b/frontend-web/webclient/tsconfig.json index c5dba279c9..6ff6571d2f 100644 --- a/frontend-web/webclient/tsconfig.json +++ b/frontend-web/webclient/tsconfig.json @@ -23,6 +23,7 @@ "allowJs": true, "moduleResolution": "bundler", "resolveJsonModule": true, + "module": "esnext", "sourceMap": true, "types": [ "vite/client", From bb50a52cee61f32e14ed3719fcc4a3aa12a6941a Mon Sep 17 00:00:00 2001 From: Schulz Date: Fri, 22 May 2026 13:07:10 +0200 Subject: [PATCH 19/19] version bump --- core2/version.txt | 2 +- frontend-web/webclient/package-lock.json | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/core2/version.txt b/core2/version.txt index 360533059e..baf2ec9e09 100644 --- a/core2/version.txt +++ b/core2/version.txt @@ -1 +1 @@ -2026.3.1-es6 +2026.3.1-es6-2 diff --git a/frontend-web/webclient/package-lock.json b/frontend-web/webclient/package-lock.json index 45f4063e77..e54ddfc65b 100644 --- a/frontend-web/webclient/package-lock.json +++ b/frontend-web/webclient/package-lock.json @@ -119,7 +119,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1551,7 +1550,6 @@ "node_modules/@svgr/core": { "version": "8.1.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -1962,7 +1960,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2278,7 +2275,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2724,7 +2720,6 @@ "node_modules/cytoscape": { "version": "3.33.1", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -3064,7 +3059,6 @@ "node_modules/d3-selection": { "version": "3.0.0", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6770,7 +6764,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6801,7 +6794,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6964,7 +6956,6 @@ "node_modules/react-redux": { "version": "9.2.0", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7113,8 +7104,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -7930,7 +7920,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7959,7 +7948,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8210,7 +8198,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8317,7 +8304,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8347,7 +8333,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" },