diff --git a/core2/version.txt b/core2/version.txt index 4129f6b3e9..baf2ec9e09 100644 --- a/core2/version.txt +++ b/core2/version.txt @@ -1 +1 @@ -2026.3.2 +2026.3.1-es6-2 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 11727b3ff0..0e6b67c6d7 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})); @@ -169,7 +169,7 @@ export const GiftSection: 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; @@ -183,7 +183,7 @@ export const GiftSection: React.FunctionComponent<{ try { await callAPI(Gifts.remove({giftId: id})); - } catch (e) { + } catch (e: any) { sendFailureNotification("Failed to delete gift: " + extractErrorMessage(e)); return; } @@ -216,59 +216,59 @@ export const GiftSection: React.FunctionComponent<{ > - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
Description{g.description}
Criteria -
    - {g.criteria.map(c => { - switch (c.type) { - case "anyone": - return
  • All UCloud users
  • - case "wayf": - return
  • Users - from {c.org}
  • - case "email": - return
  • @{c.domain}
  • - } - })} -
-
Resources -
    - {g.resources.map((r, idx) => { - const pc = (state.remoteData.managedProducts ?? {})[r.provider]?.find(it => it.name === r.category); - if (!pc) return null; - return
  • - {r.category} / {r.provider}: {Accounting.balanceToString(pc, r.balanceRequested)} -
  • - })} -
-
Granted - {g.renewEvery == 0 ? "Once" : (g.renewEvery == 1 ? "Every month" : "Every " + g.renewEvery.toString() + " months")} -
Delete - -
Description{g.description}
Criteria +
    + {g.criteria.map(c => { + switch (c.type) { + case "anyone": + return
  • All UCloud users
  • + case "wayf": + return
  • Users + from {c.org}
  • + case "email": + return
  • @{c.domain}
  • + } + })} +
+
Resources +
    + {g.resources.map((r, idx) => { + const pc = (state.remoteData.managedProducts ?? {})[r.provider]?.find(it => it.name === r.category); + if (!pc) return null; + return
  • + {r.category} / {r.provider}: {Accounting.balanceToString(pc, r.balanceRequested)} +
  • + })} +
+
Granted + {g.renewEvery == 0 ? "Once" : (g.renewEvery == 1 ? "Every month" : "Every " + g.renewEvery.toString() + " months")} +
Delete + +
@@ -282,7 +282,7 @@ export const GiftSection: React.FunctionComponent<{
+ 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/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/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/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/Dashboard/Dashboard.tsx b/frontend-web/webclient/app/Dashboard/Dashboard.tsx index a08da0ff74..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, @@ -92,10 +84,16 @@ function Dashboard(): React.ReactNode { useSetRefreshFunction(reload); const main = (
- + + + + - +
@@ -129,11 +127,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 +139,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 +149,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 +197,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 +288,10 @@ function DashboardResources({wallets}: { - + {category.name} @@ -291,7 +306,9 @@ function DashboardResources({wallets}: { - + + + } @@ -309,22 +326,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 +383,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 +421,7 @@ const NewsClass = injectStyle("with-graphic", k => ` display: flex; height: 270px; } - + ${k}.halric { flex-wrap: wrap; height: unset; @@ -396,7 +432,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 +446,7 @@ const NewsClass = injectStyle("with-graphic", k => ` position: relative; top: -112px; } - + ${k} h5 { margin: 0; margin-bottom: 10px; @@ -421,19 +457,13 @@ const NewsClass = injectStyle("with-graphic", k => ` display: none; width: 0px; } - + ${k} > div { width: 100%; } } `); -function reduxOperations(dispatch: Dispatch): DashboardOperations { - return { - setAllLoading: loading => dispatch(setAllLoading(loading)), - }; -} - const DashboardCard: React.FunctionComponent<{ title: string; linkTo?: string; @@ -441,15 +471,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 deleted file mode 100644 index 650b6f9402..0000000000 --- a/frontend-web/webclient/app/Dashboard/Redux/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import {SetLoadingAction} from "@/Types"; -import {DashboardStateProps} from "@/Dashboard"; -import {initDashboard} from "@/DefaultObjects"; -import {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 SET_ALL_LOADING = "SET_ALL_LOADING"; -export const DASHBOARD_RECENT_JOBS_ERROR = "DASHBOARD_RECENT_JOBS_ERROR"; - -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; - } - } -} diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index 99f4ef9c89..cc2aa47c5e 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; } /** @@ -21,15 +23,13 @@ export interface StatusReduxObject { */ export interface LegacyReduxObject { hookStore: HookStore; - dashboard: DashboardStateProps; status: StatusReduxObject; avatar: AvatarReduxObject; project: ProjectRedux.State; terminal: TerminalState; providerBrandings: ProviderBrandingResponse; branding: BrandingResponse - popinChild: PopInArgs | null; - loading: boolean; + popinChild: PopInArgs; sidebar: SidebarStateProps; } @@ -41,7 +41,8 @@ declare global { export function initStatus(): StatusReduxObject { return ({ title: "", - loading: false + loading: false, + tab: SidebarTabId.NONE, }); } @@ -54,15 +55,13 @@ export function initDashboard(): DashboardStateProps { export function initObject(): ReduxObject { return { hookStore: {}, - dashboard: initDashboard(), status: initStatus(), avatar: initAvatar(), project: ProjectRedux.initialState, terminal: initTerminalState(), providerBrandings: initProviderBranding(), branding: initBranding(), - popinChild: null, - loading: false, + popinChild: {el: undefined}, 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 a4452af74f..d3d3c7a302 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"; @@ -208,7 +208,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, @@ -310,7 +310,7 @@ const DriveBrowse: React.FunctionComponent<{ browser.renderRows(); dialogStore.success(); window.dispatchEvent(new CustomEvent(DriveChange)); - } 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..dac8e31000 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,8 @@ 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"; +import {genericSet} from "@/Utilities/ReduxHooks"; export enum SensitivityLevel { "INHERIT" = "Inherit", @@ -681,8 +683,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); @@ -1238,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]; @@ -1633,7 +1636,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..6338f5d93f 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, @@ -92,8 +92,8 @@ export function FileTree({tree, onTreeAction, onNodeActivated, root, ...props}: forceEvaluationOnOpen={true} openFnRef={openOperations} selected={[]} + row={42 as any} // This works, for some reason 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 7ba8c23edd..93d0967f89 100644 --- a/frontend-web/webclient/app/Login/Login.tsx +++ b/frontend-web/webclient/app/Login/Login.tsx @@ -154,7 +154,7 @@ export const LoginPage: React.FC<{initialState?: any}> = props => { } handleAuthState(await response.json()); - } catch (e) { + } catch (e: any) { sendFailureNotification( errorMessageOrDefault({ request: e, @@ -212,7 +212,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); @@ -252,7 +252,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({ 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/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/Project/ProjectSwitcher.tsx b/frontend-web/webclient/app/Project/ProjectSwitcher.tsx index f6ca79ffd2..1919f95b21 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, 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..6a41e44011 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.providers = action.payload.providers; + } } -} \ 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 8d67c745e8..273f074363 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 {TerminalAction, TerminalState, TerminalTab, useTerminalDispatcher, 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"; @@ -13,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} from "@reduxjs/toolkit"; const Wrapper = injectStyle("wrapper", k => ` ${k} { @@ -83,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); @@ -121,19 +123,19 @@ 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]); - 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); @@ -144,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 => { @@ -173,8 +175,7 @@ export const TerminalContainer: React.FunctionComponent = () => { forceEvaluationOnOpen={true} openFnRef={openTabOperationWindow} selected={[]} - extra={null} - row={42} + extra={undefined} hidden location={"IN_ROW"} /> @@ -192,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, }, @@ -207,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, @@ -215,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, @@ -227,9 +228,9 @@ function tabOperations(dispatch: (action: TerminalAction) => void, tabIdx: numbe { 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({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 e15365f6fa..9a2c567ea3 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 d5d6f372da..b82a6573ee 100644 --- a/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx +++ b/frontend-web/webclient/app/UCloud/FileCollectionsApi.tsx @@ -123,7 +123,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 d91dd79a7a..5e9e17d20f 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"); @@ -202,7 +204,7 @@ class FilesApi extends ResourceApi & ExtraFileCallbacks> = { + renderer: ItemRenderer & ExtraFileCallbacks> = { }; private defaultRetrieveFlags: Partial = { @@ -237,10 +239,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, @@ -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>[]); } 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(); @@ -1316,12 +1317,12 @@ 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.`); } } - 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; @@ -1362,8 +1363,8 @@ export function FilePreview({initialFile}: { const providerTitle = getProviderTitle(providerId) ?? providerId; const folder = getParentPath(initialFile.id); - dispatch({type: "TerminalOpen"}); - dispatch({type: "TerminalOpenTab", payload: {tab: {title: providerTitle, folder}}}); + dispatch(terminalOpen()); + dispatch(terminalOpenTab({tab: {title: providerTitle, folder}})); }, [drive, initialFile]); const newFolder = useCallback(async (path: string) => { @@ -1419,7 +1420,7 @@ export function FilePreview({initialFile}: { return success; }, []); - const operations = useCallback((file?: VirtualFile): Operation[] => { + const operations = useCallback((file?: VirtualFile): Operation[] => { const reload = () => { let path: string; if (file) { 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 277d2e0671..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,14 +55,6 @@ export interface Provider extends Resource void; -} - class ProviderApi extends ResourceApi { routingNamespace = "providers"; @@ -92,7 +81,7 @@ class ProviderApi extends ResourceApi & ProviderCallbacks>[] { + 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/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, diff --git a/frontend-web/webclient/app/Utilities/ReduxHooks.ts b/frontend-web/webclient/app/Utilities/ReduxHooks.ts index 707487b8ee..19236c165a 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: keyof HookStore; + 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..7a2d68b4a0 100644 --- a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx @@ -1,16 +1,15 @@ import {useEffect} from "react"; -import {Action, AnyAction, combineReducers} 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"; 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,43 +22,35 @@ 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, 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)[] = []; 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 b7b530aa3b..af5642ad27 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..cd2cd079d8 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,16 @@ const PopInClass = injectStyle("popin-class", k => ` `); export function RightPopIn(): React.ReactNode { - const dispatch = useDispatch(); - const content = useSelector(it => it.popinChild); + const dispatch = useDispatch(); /* Alternatively, use React.portal */ - return + return dispatch(setPopInChild(null))} />} + left={ dispatch(setPopInChild({el: undefined}))} />} right={content?.onFullScreen ? { content?.onFullScreen?.(); - dispatch(setPopInChild(null)); + dispatch(setPopInChild({el: undefined})); }} /> : null} /> @@ -58,19 +57,24 @@ 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.el = action.payload.el; + state.onFullScreen = action.payload.onFullScreen; + + } } -}; \ 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 1c0f15e43f..7ff9bf5d3d 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 { @@ -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; @@ -279,11 +277,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 +296,7 @@ function UserMenuExternalLink(props: { if (!props.text) return null; return
- + {props.text}
@@ -336,7 +334,7 @@ function UserMenu({branding, avatar, dialog, setOpenDialog}: { closeFnRef={close} colorOnHover={false} trigger={Client.isLoggedIn ? - : null} + : null} > {branding.statusPage ? ( @@ -349,18 +347,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 +366,7 @@ function UserMenu({branding, avatar, dialog, setOpenDialog}: { Client.logout()} data-component={"logout-button"}> - + Logout @@ -381,7 +379,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 +458,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 +492,8 @@ export function Sidebar(): React.ReactNode {
- + onClick={onLogoClick}> +
to ? ( + key={label} to={typeof to === "function" ? to() : to}>
setHoveredPage(label)} @@ -519,20 +518,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} + > +
)}
@@ -625,7 +624,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( @@ -785,7 +784,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 => ); } @@ -838,13 +837,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, + }]; } @@ -852,22 +851,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; @@ -877,13 +876,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(); @@ -1038,17 +1037,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) => { @@ -1089,10 +1083,10 @@ function SecondarySidebar({ setSelectedPage(hovered)}> - + height="38px" width={"30px"} + justifyContent={"center"} borderRadius="12px 0 0 12px" + onClick={clicked ? onClear : () => setSelectedPage(hovered)}> + @@ -1100,7 +1094,7 @@ function SecondarySidebar({ {active !== SidebarTabId.FILES ? null : <> Drives + tab={SidebarTabId.FILES}>Drives {(!canConsume || drives.data.items.length === 0) && <> No drives available } @@ -1110,8 +1104,8 @@ function SecondarySidebar({ key={drive.id} text={drive.specification.title} icon={isShare(drive) ? - : - } + : + } to={AppRoutes.files.drive(drive.id)} tab={SidebarTabId.FILES} /> @@ -1133,18 +1127,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. */} @@ -1156,7 +1150,7 @@ function SecondarySidebar({ key={fav.metadata.name} to={AppRoutes.jobs.create(fav.metadata.name)} text={fav.metadata.title} - icon={} + icon={} tab={SidebarTabId.APPLICATIONS} /> )} @@ -1183,7 +1177,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 && <> @@ -1200,7 +1194,7 @@ function SecondarySidebar({ key={run.id} to={AppRoutes.jobs.view(run.id)} text={name} - icon={} + icon={} tab={SidebarTabId.RUNS} /> })} @@ -1209,30 +1203,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 { @@ -1265,7 +1259,7 @@ function Username(): React.ReactNode { )} > - This is your username.

+ This is your username.

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

+ This is your project ID.

Click to copy to clipboard. } @@ -1341,8 +1335,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 3a050a74e8..e54ddfc65b 100644 --- a/frontend-web/webclient/package-lock.json +++ b/frontend-web/webclient/package-lock.json @@ -25,22 +25,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.15.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", @@ -53,7 +53,7 @@ "yaml": "2.8.3" }, "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" } }, @@ -951,13 +951,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" @@ -3559,10 +3559,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": { @@ -4173,13 +4179,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", @@ -6631,13 +6634,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" @@ -6650,9 +6653,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": { @@ -6757,9 +6760,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" @@ -6787,15 +6790,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": { @@ -6991,9 +6994,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", @@ -7013,12 +7016,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" @@ -7940,7 +7943,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": { diff --git a/frontend-web/webclient/package.json b/frontend-web/webclient/package.json index 447f9d3ca4..411d5171c8 100644 --- a/frontend-web/webclient/package.json +++ b/frontend-web/webclient/package.json @@ -39,22 +39,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.15.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", @@ -67,7 +67,7 @@ "yaml": "2.8.3" }, "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", @@ -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..6ff6571d2f 100644 --- a/frontend-web/webclient/tsconfig.json +++ b/frontend-web/webclient/tsconfig.json @@ -4,29 +4,29 @@ "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" + "dom", + "ES2023" ], "skipLibCheck": true, - "target": "es2015", + "target": "es2020", "jsx": "react", "pretty": true, "allowJs": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, + "module": "esnext", "sourceMap": true, - "baseUrl": ".", "types": [ "vite/client", - "monaco-editor/monaco", ], "paths": { "@/*": [ @@ -36,6 +36,8 @@ }, "exclude": [ "./__tests__/*", - "./dist/*" + "./dist/*", + "./playwright.config.ts", + "./tests/*" ] } \ No newline at end of file