diff --git a/frontend-web/webclient/app/DefaultObjects.ts b/frontend-web/webclient/app/DefaultObjects.ts index 99f4ef9c89..bec2cb1082 100644 --- a/frontend-web/webclient/app/DefaultObjects.ts +++ b/frontend-web/webclient/app/DefaultObjects.ts @@ -6,8 +6,6 @@ import {SidebarStateProps} from "./Applications/Redux/Reducer"; import {getUserThemePreference} from "./UtilityFunctions"; import {defaultAvatar} from "./AvataaarLib"; import {HookStore} from "./Utilities/ReduxHooks"; -import {ProviderBrandingResponse} from "./UCloud/ProviderBrandingApi"; -import {initProviderBranding} from "./ProviderBrandings/AutomaticProviderBranding"; import {BrandingResponse} from "./UCloud/BrandingApi"; import {initBranding} from "./Applications/Branding/AutomaticBranding"; @@ -26,7 +24,6 @@ export interface LegacyReduxObject { avatar: AvatarReduxObject; project: ProjectRedux.State; terminal: TerminalState; - providerBrandings: ProviderBrandingResponse; branding: BrandingResponse popinChild: PopInArgs | null; loading: boolean; @@ -59,7 +56,6 @@ export function initObject(): ReduxObject { avatar: initAvatar(), project: ProjectRedux.initialState, terminal: initTerminalState(), - providerBrandings: initProviderBranding(), branding: initBranding(), popinChild: null, loading: false, diff --git a/frontend-web/webclient/app/Grants/Editor.tsx b/frontend-web/webclient/app/Grants/Editor.tsx index 941d49da41..e118f61a5a 100644 --- a/frontend-web/webclient/app/Grants/Editor.tsx +++ b/frontend-web/webclient/app/Grants/Editor.tsx @@ -47,6 +47,7 @@ import {ChangeOrganizationDetails, OptionalInfo, optionalInfoRequest, optionalIn import {ProviderBranding, ProviderBrandingProductDescription, ProviderBrandingResponse} from "@/UCloud/ProviderBrandingApi"; import {useSelector} from "react-redux"; import {sendFailureNotification, sendSuccessNotification} from "@/Notifications"; +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; // State model // ===================================================================================================================== @@ -1371,7 +1372,6 @@ export function Editor(): React.ReactNode { const isForSubAllocator = getQueryParam(location.search, "subAllocator") == "true"; useProjectId(); // FIXME(Jonas): Is this some refresh-thing that breaks stuff if you remove it? - const providerBrandingData = useSelector((it: ReduxObject) => it.providerBrandings); const [missingUserInfo, setMissingUserInfo] = React.useState(false); React.useEffect(() => { (async () => { @@ -2139,7 +2139,7 @@ export function Editor(): React.ReactNode { if (hideZeroFields && !anyNonZeroValues) return null; - const currentProvider = providerBrandingData.providers[providerId]; + const currentProvider = providerBrandingStore.getSnapshot().providers[providerId]; const productDescription = currentProvider?.productDescription?.find(it => it.category === category.category.name); const showDescriptions = productDescription != undefined; diff --git a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx index ef0337c187..d15f01a87c 100644 --- a/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx +++ b/frontend-web/webclient/app/ProviderBrandings/AutomaticProviderBranding.tsx @@ -1,50 +1,39 @@ -import * as React from "react"; -import {useCloudAPI} from "@/Authentication/DataHook"; -import {providerBrandingApi, ProviderBrandingResponse } from "@/UCloud/ProviderBrandingApi"; -import {useDispatch} from "react-redux"; -import {PayloadAction} from "@reduxjs/toolkit"; +import {callAPI} from "@/Authentication/DataHook"; +import {ProviderBranding, providerBrandingApi, ProviderBrandingResponse} from "@/UCloud/ProviderBrandingApi"; +import {ExternalStoreBase} from "@/Utilities/ReduxUtilities"; +import ProviderInfo from "@/Assets/provider_info.json"; -export const AutomaticProviderBranding: React.FunctionComponent = () => { - const [providerBrandings, fetchBranding] = useCloudAPI( - providerBrandingApi.browse(), - { providers: {} } - ); - React.useEffect(() => { - const intervalId = setInterval(() => { - fetchBranding(providerBrandingApi.browse()); - }, 1000 * 60 * 60); - return () => { - clearInterval(intervalId); - } - }, []); - - const dispatch = useDispatch(); +export const providerBrandingStore = new class extends ExternalStoreBase { + private branding: ProviderBrandingResponse = {providers: {}}; - React.useEffect(() => { - dispatch({type: ADD_PROVIDER_BRANDING, payload: providerBrandings.data}); - }, [providerBrandings.data]); - - return null; -}; + async onInit(): Promise { + this.fetch(); + window.setInterval(() => { + this.fetch(); + }, 3 * 600_000); + } -const ADD_PROVIDER_BRANDING = "ADD_PROVIDER_BRANDING"; -type SetProviderBranding = PayloadAction + async fetch() { + try { + const response = await callAPI(providerBrandingApi.browse()); + this.branding = response; + } catch (e: any) { + console.warn(e); + } + } -type ProviderBrandingAction = SetProviderBranding; + public getSnapshot(): Readonly { + return this.branding; + } -export function initProviderBranding(): ProviderBrandingResponse { - return { - providers: {}, + public getProviderProperty(providerId: string, providerProperty: Property): ProviderBranding[Property] | undefined { + const property = this.branding.providers[providerId]?.[providerProperty]; + const useFallback = providerProperty === "logo" && (property as string)?.includes("/"); + if (!property) console.warn(`Property '${providerProperty}' missing for ${providerId}`, this.branding); + if (useFallback) console.warn(`Using fallback logo for ${providerId}. Actual value: ${property}`) + return property && !useFallback ? property : ProviderInfo.providers.find(it => it.id === providerId)?.[providerProperty as string]; } } -export function providerBrandingReducer(state: ProviderBrandingResponse = initProviderBranding(), action: ProviderBrandingAction): ProviderBrandingResponse { - switch (action.type) { - case ADD_PROVIDER_BRANDING: { - return action.payload; - } - default: - return state; - } -} \ No newline at end of file +providerBrandingStore.onInit(); diff --git a/frontend-web/webclient/app/Providers/Detailed.tsx b/frontend-web/webclient/app/Providers/Detailed.tsx index 5cc0c82509..57f5988f92 100644 --- a/frontend-web/webclient/app/Providers/Detailed.tsx +++ b/frontend-web/webclient/app/Providers/Detailed.tsx @@ -10,13 +10,13 @@ import {ProviderLogo} from "./ProviderLogo"; import {ProviderTitle} from "./ProviderTitle"; import TitledCard from "@/ui-components/HighlightedCard"; import {SidebarTabId} from "@/ui-components/SidebarComponents"; -import {useSelector} from "react-redux"; import {ProviderBranding} from "@/UCloud/ProviderBrandingApi"; +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; function useProviderBranding(id?: string): ProviderBranding | undefined { - const data = useSelector((it: ReduxObject) => it.providerBrandings); if (!id) return undefined; - return data.providers[id]; + const providers = providerBrandingStore.getSnapshot(); + return providers.providers[id]; } export default function DetailedProvider() { @@ -50,7 +50,7 @@ export default function DetailedProvider() { {section.image !== "" ? - {`Provider + {`Provider : }
@@ -62,27 +62,27 @@ export default function DetailedProvider() { )} - {entry.productDescription.map((prod, index) => + {entry.productDescription.map((prod, index) =>

{prod.category}

- +
{prod.shortDescription}
-
- {prod.section.image ? {`Product :
} -
-
-
-
- - {prod.section.description} - -
+
+ {prod.section.image ? {`Product :
} +
+
+
+
+ + {prod.section.description} + +
diff --git a/frontend-web/webclient/app/Providers/Overview.tsx b/frontend-web/webclient/app/Providers/Overview.tsx index da018b3a00..13e7e3aa38 100644 --- a/frontend-web/webclient/app/Providers/Overview.tsx +++ b/frontend-web/webclient/app/Providers/Overview.tsx @@ -12,8 +12,9 @@ import {CardClass} from "@/ui-components/Card"; import {SidebarTabId} from "@/ui-components/SidebarComponents"; import {ProviderBranding} from "@/UCloud/ProviderBrandingApi"; import {useSelector} from "react-redux"; +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; -export function ProviderEntry(props: { provider: ProviderBranding }): React.ReactNode { +export function ProviderEntry(props: {provider: ProviderBranding}): React.ReactNode { if (!props.provider.id || !props.provider.title) return null; return ( @@ -21,11 +22,11 @@ export function ProviderEntry(props: { provider: ProviderBranding }): React.Reac
- +

- +

@@ -37,8 +38,8 @@ export function ProviderEntry(props: { provider: ProviderBranding }): React.Reac } function useProviderBrandings(): Record | undefined { - const data = useSelector((it: ReduxObject) => it.providerBrandings); - return data.providers; + const providers = React.useSyncExternalStore(sub => providerBrandingStore.subscribe(sub), providerBrandingStore.getSnapshot); + return providers.providers; } export default function ProviderOverview() { @@ -51,20 +52,20 @@ export default function ProviderOverview() { const main = {Object.values(providers).map(provider => - + )} if (!Client.isLoggedIn) return (<> - - + +
{main}
); - return (); + return (); } const ProviderCard = injectStyle("provider-card", k => ` diff --git a/frontend-web/webclient/app/Providers/ProviderLogo.tsx b/frontend-web/webclient/app/Providers/ProviderLogo.tsx index 91c2c2bb47..4f7551373b 100644 --- a/frontend-web/webclient/app/Providers/ProviderLogo.tsx +++ b/frontend-web/webclient/app/Providers/ProviderLogo.tsx @@ -1,21 +1,26 @@ import * as React from "react"; import {Image} from "@/ui-components"; -import ProviderInfo from "@/Assets/provider_info.json"; import {classConcat} from "@/Unstyled"; import {injectStyle} from "@/Unstyled"; import {TooltipV2} from "@/ui-components/Tooltip"; import {getProviderTitle} from "@/Providers/ProviderTitle"; +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; export function providerLogoPath(providerId: string): string { - const logo = ProviderInfo.providers.find(p => p.id === providerId)?.logo ?? ""; + const logo = providerBrandingStore.getProviderProperty(providerId, "logo"); if (logo) return `/Images/${logo}`; return ""; } export const ProviderLogo: React.FunctionComponent<{providerId: string; size: number; className?: string;}> = ({providerId, size, className}) => { - const myInfo = ProviderInfo.providers.find(p => p.id === providerId); + const [logo, title] = [ + providerBrandingStore.getProviderProperty(providerId, "logo"), + providerBrandingStore.getProviderProperty(providerId, "title") + ]; + + return - {!myInfo ? (providerId[0] ?? "?").toUpperCase() : {`Logo} + {!logo ? (providerId[0] ?? "?").toUpperCase() : {`Logo} }; diff --git a/frontend-web/webclient/app/Providers/ProviderTitle.tsx b/frontend-web/webclient/app/Providers/ProviderTitle.tsx index 301a10cd19..5faaab055f 100644 --- a/frontend-web/webclient/app/Providers/ProviderTitle.tsx +++ b/frontend-web/webclient/app/Providers/ProviderTitle.tsx @@ -1,25 +1,16 @@ import * as React from "react"; -import ProviderInfo from "@/Assets/provider_info.json"; import {capitalized} from "@/UtilityFunctions"; - -interface ProviderInfo { - id: string; - title: string; - logo: string | null; - shortTitle: string; -} +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; export const ProviderTitle: React.FunctionComponent<{providerId: string}> = ({providerId}) => { return <>{getProviderTitle(providerId)}; }; export function getProviderTitle(providerId: string): string { - const providers: ProviderInfo[] = ProviderInfo.providers; - const myInfo = providers.find(p => p.id === providerId); - return myInfo?.title ?? capitalized(providerId.replace("_", " ").replace("-", " ")); + const title = providerBrandingStore.getProviderProperty(providerId, "title"); + return title ?? capitalized(providerId.replace("_", " ").replace("-", " ")); } export function getShortProviderTitle(providerId: string): string { - const providers: ProviderInfo[] = ProviderInfo.providers; - const myInfo = providers.find(p => p.id === providerId); - return myInfo?.shortTitle ?? capitalized(providerId.replace("_", " ").replace("-", " ")); + const shortTitle = providerBrandingStore.getProviderProperty(providerId, "shortTitle"); + return shortTitle ?? capitalized(providerId.replace("_", " ").replace("-", " ")); } diff --git a/frontend-web/webclient/app/Terminal/Container.tsx b/frontend-web/webclient/app/Terminal/Container.tsx index ac7a4f1b8c..77151d1a47 100644 --- a/frontend-web/webclient/app/Terminal/Container.tsx +++ b/frontend-web/webclient/app/Terminal/Container.tsx @@ -9,7 +9,7 @@ import {BulkResponse} from "@/UCloud"; import JobsApi, {InteractiveSession} from "@/UCloud/JobsApi"; import {bulkRequestOf, bulkResponseOf} from "@/UtilityFunctions"; import {ShellWithSession} from "@/Applications/Jobs/Shell"; -import {Terminal} from "xterm"; +import {Terminal} from "@xterm/xterm"; import {getCssPropertyValue} from "@/Utilities/StylingUtilities"; import {CSSVarCurrentSidebarStickyWidth} from "@/ui-components/List"; import {Tab} from "@/Editor/Editor"; diff --git a/frontend-web/webclient/app/UCloud/ProviderBrandingApi.tsx b/frontend-web/webclient/app/UCloud/ProviderBrandingApi.tsx index fa4f51da4e..e7819ea119 100644 --- a/frontend-web/webclient/app/UCloud/ProviderBrandingApi.tsx +++ b/frontend-web/webclient/app/UCloud/ProviderBrandingApi.tsx @@ -35,5 +35,4 @@ export class ProviderBrandingApi { } } -const providerBrandingApi = new ProviderBrandingApi(); -export { providerBrandingApi }; \ No newline at end of file +export const providerBrandingApi = new ProviderBrandingApi(); \ No newline at end of file diff --git a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx index 25d9564eb3..aabadc4d98 100644 --- a/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx +++ b/frontend-web/webclient/app/Utilities/ReduxUtilities.tsx @@ -12,7 +12,6 @@ import {popInReducer} from "@/ui-components/PopIn"; import sidebar from "@/Applications/Redux/Reducer"; import {EnhancedStore, ReducersMapObject, configureStore} from "@reduxjs/toolkit"; import {noopCall} from "@/Authentication/DataHook"; -import {providerBrandingReducer} from "@/ProviderBrandings/AutomaticProviderBranding"; import {brandingReducer} from "@/Applications/Branding/AutomaticBranding"; export const CONTEXT_SWITCH = "CONTEXT_SWITCH"; @@ -42,7 +41,6 @@ export const store = confStore(initObject(), { sidebar, avatar: avatarReducer, terminal: terminalReducer, - providerBrandings: providerBrandingReducer, branding: brandingReducer, loading, project: ProjectRedux.reducer, diff --git a/frontend-web/webclient/app/UtilityFunctions.tsx b/frontend-web/webclient/app/UtilityFunctions.tsx index d472bb667f..c6faa1e6c6 100644 --- a/frontend-web/webclient/app/UtilityFunctions.tsx +++ b/frontend-web/webclient/app/UtilityFunctions.tsx @@ -66,16 +66,6 @@ export type ExtensionType = | "markdown" | "application"; -export const commonFileExtensions = [ - "app", "application", "md", "markdown", "markdown", "zig", "swift", "kt", "kts", "js", "jsx", "ts", "tsx", - "java", "py", "python", "tex", "r", "c", "h", "cc", "hh", "c++", "h++", "hpp", "cpp", "cxx", "jai", "hxx", - "html", "htm", "lhs", "hs", "sql", "sh", "iol", "ol", "col", "bib", "toc", "jar", "exe", "xml", "json", - "yml", "ini", "sbatch", "code", "png", "gif", "tiff", "eps", "ppm", "svg", "jpg", "jpeg", "image", "txt", - "doc", "docx", "log", "csv", "tsv", "plist", "out", "err", "text", "pdf", "pdf", "wav", "mp3", "ogg", "aac", - "pcm", "audio", "mpg", "mp4", "avi", "mov", "wmv", "video", "gz", "zip", "tar", "tgz", "tbz", "bz2", "archive", - "dat", "binary", "rs", -]; - const languages = { "md": "markdown", "kt": "kotlin", diff --git a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx index 2a4b7b586c..38cfd9144a 100644 --- a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx +++ b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx @@ -32,7 +32,6 @@ import {createPortal} from "react-dom"; import {ProjectSwitcher, FilterInputClass, projectCache} from "@/Project/ProjectSwitcher"; import {addProjectListener, removeProjectListener} from "@/Project/ReduxState"; import {ProductType, ProductV2} from "@/Accounting"; -import ProviderInfo from "@/Assets/provider_info.json"; import {ProductSelector} from "@/Products/Selector"; import {Client} from "@/Authentication/HttpClientInstance"; import {divHtml, divText, image} from "@/Utilities/HTMLUtilities"; @@ -50,6 +49,7 @@ import {callAPI, 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"; +import {providerBrandingStore} from "@/ProviderBrandings/AutomaticProviderBranding"; const CLEAR_FILTER_VALUE = "\n\nCLEAR_FILTER\n\n"; const UTILITY_COLOR: ThemeColor = "textPrimary"; @@ -3661,8 +3661,8 @@ export function resourceCreationWithProductSelector( return {startCreation, cancelCreation, portal}; } -export function providerIcon(providerId: string, opts?: Partial, logo?: string): HTMLElement { - const myInfo: {logo: string} | undefined = logo ? {logo} : ProviderInfo.providers.find(p => p.id === providerId); +export function providerIcon(providerId: string, opts?: Partial, providedLogo?: string): HTMLElement { + const logo = providedLogo ?? providerBrandingStore.getProviderProperty(providerId, "logo"); const outer = divHtml(""); outer.className = "provider-icon" outer.style.background = "var(--secondaryMain)"; @@ -3676,9 +3676,9 @@ export function providerIcon(providerId: string, opts?: Partial - diff --git a/frontend-web/webclient/package-lock.json b/frontend-web/webclient/package-lock.json index 3a050a74e8..f9df74351b 100644 --- a/frontend-web/webclient/package-lock.json +++ b/frontend-web/webclient/package-lock.json @@ -119,6 +119,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1550,6 +1551,7 @@ "node_modules/@svgr/core": { "version": "8.1.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -1960,6 +1962,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2275,6 +2278,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2720,6 +2724,7 @@ "node_modules/cytoscape": { "version": "3.33.1", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -3059,6 +3064,7 @@ "node_modules/d3-selection": { "version": "3.0.0", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -6761,6 +6767,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6791,6 +6798,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6953,6 +6961,7 @@ "node_modules/react-redux": { "version": "9.2.0", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7101,7 +7110,8 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -7917,6 +7927,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7943,6 +7954,7 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8193,6 +8205,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8299,6 +8312,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8328,6 +8342,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" },