From 3f4826849222d9d83fb63a63b2a6981cffc7c13a Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 12 Jan 2026 13:59:12 +0100 Subject: [PATCH 01/11] Do not use offset for the first page for performance reasons --- web/src/core/adapters/sqlOlap/sqlOlap.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/core/adapters/sqlOlap/sqlOlap.ts b/web/src/core/adapters/sqlOlap/sqlOlap.ts index 1ecbd6018..e9ac98883 100644 --- a/web/src/core/adapters/sqlOlap/sqlOlap.ts +++ b/web/src/core/adapters/sqlOlap/sqlOlap.ts @@ -278,7 +278,7 @@ export const createDuckDbSqlOlap = (params: { sourceUrl = sourceUrl_noRedirect; } - const sqlQuery = `SELECT * FROM ${(() => { + let sqlQuery = `SELECT * FROM ${(() => { switch (fileType) { case "csv": return `read_csv('${sourceUrl}')`; @@ -287,7 +287,11 @@ export const createDuckDbSqlOlap = (params: { case "json": return `read_json('${sourceUrl}')`; } - })()} LIMIT ${rowsPerPage} OFFSET ${rowsPerPage * (page - 1)}`; + })()} LIMIT ${rowsPerPage}`; + + if (page !== 1) { + sqlQuery += ` OFFSET ${rowsPerPage * (page - 1)}`; + } const conn = await db.connect(); const stmt = await conn.prepare(sqlQuery); From e53aada2c5fccb8459389f36f70964f224756843 Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 12 Jan 2026 15:49:29 +0100 Subject: [PATCH 02/11] Stop re-mounting the explorer component on every navigation --- web/src/ui/pages/s3Explorer/Explorer.tsx | 25 +++--------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/web/src/ui/pages/s3Explorer/Explorer.tsx b/web/src/ui/pages/s3Explorer/Explorer.tsx index 55f61eb2f..9e2a43803 100644 --- a/web/src/ui/pages/s3Explorer/Explorer.tsx +++ b/web/src/ui/pages/s3Explorer/Explorer.tsx @@ -152,6 +152,8 @@ function Explorer_inner(props: Props) { const onOpenFile = useConstCallback( ({ basename }) => { + assert(isCurrentWorkingDirectoryLoaded); + //TODO use dataExplorer thunk if ( basename.endsWith(".parquet") || @@ -160,7 +162,7 @@ function Explorer_inner(props: Props) { ) { routes .dataExplorer({ - source: `s3://${directoryPath.replace(/\/$/g, "")}/${basename}` + source: `s3://${currentWorkingDirectoryView.directoryPath.replace(/\/$/g, "")}/${basename}` }) .push(); return; @@ -180,27 +182,6 @@ function Explorer_inner(props: Props) { const { cx, css, theme } = useStyles(); - if ( - isCurrentWorkingDirectoryLoaded && - currentWorkingDirectoryView.directoryPath !== directoryPath - ) { - return ( -
- -
- ); - } - if (!isCurrentWorkingDirectoryLoaded) { return (
Date: Mon, 12 Jan 2026 15:51:41 +0100 Subject: [PATCH 03/11] Better positioning of the explorer --- web/src/ui/pages/s3Explorer/Page.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/src/ui/pages/s3Explorer/Page.tsx b/web/src/ui/pages/s3Explorer/Page.tsx index fbbbbccd9..9586932dd 100644 --- a/web/src/ui/pages/s3Explorer/Page.tsx +++ b/web/src/ui/pages/s3Explorer/Page.tsx @@ -90,8 +90,7 @@ function S3Explorer() {
@@ -433,9 +432,13 @@ function S3ProfileSelect() { const useStyles = tss.withName({ S3Explorer }).create(({ theme }) => ({ root: { - height: "100%" + height: "100%", + display: "flex", + flexDirection: "column", + overflow: "auto" }, explorer: { - marginTop: theme.spacing(4) + marginTop: theme.spacing(4), + flex: 1 } })); From 00cf901138770974631e30ae6dbd525e4e6410c9 Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 12 Jan 2026 16:03:47 +0100 Subject: [PATCH 04/11] Fix navigation layout shift --- web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx b/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx index 5ac4cdf01..e90b92cd4 100644 --- a/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx +++ b/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx @@ -442,7 +442,14 @@ export const Explorer = memo((props: ExplorerProps) => { ); if (bookmarkStatus.isBookmarked && bookmarkStatus.isReadonly) { - return ; + return ( + + ); } return ( From 52724d9c84323e2b765a0fb7dda0fee07f9aacae Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 12 Jan 2026 16:13:12 +0100 Subject: [PATCH 05/11] Only offer 25 and 50 row in explorer --- web/src/ui/pages/dataExplorer/DataGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/pages/dataExplorer/DataGrid.tsx b/web/src/ui/pages/dataExplorer/DataGrid.tsx index 9c89481ee..e7e2a380e 100644 --- a/web/src/ui/pages/dataExplorer/DataGrid.tsx +++ b/web/src/ui/pages/dataExplorer/DataGrid.tsx @@ -140,7 +140,7 @@ export function DataGrid(params: { className?: string }) { paginationMode="server" rowCount={rowCount ?? 999999999} pageSizeOptions={(() => { - const pageSizeOptions = [25, 50, 100]; + const pageSizeOptions = [25, 50]; assert(pageSizeOptions.includes(rowsPerPage)); return pageSizeOptions; })()} From 659f3b187bd5e8bcd62386f7af3da8cde36cd0e4 Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 12 Jan 2026 16:26:15 +0100 Subject: [PATCH 06/11] Improve breadcrumb navigation --- .../pages/fileExplorer/Explorer/Explorer.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx b/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx index e90b92cd4..1009bdf92 100644 --- a/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx +++ b/web/src/ui/pages/fileExplorer/Explorer/Explorer.tsx @@ -47,6 +47,7 @@ import { ExplorerDownloadSnackbar } from "./ExplorerDownloadSnackbar"; import { IconButton } from "onyxia-ui/IconButton"; import { Icon } from "onyxia-ui/Icon"; import { getIconUrlByName } from "lazy-icons"; +import { keyframes } from "tss-react"; export type ExplorerProps = { /** @@ -424,13 +425,6 @@ export const Explorer = memo((props: ExplorerProps) => { onNavigate={onBreadcrumbNavigate} evtAction={evtBreadcrumbAction} /> - {isNavigating && ( - - )} {(() => { if (bookmarkStatus === undefined) { return null; @@ -459,6 +453,13 @@ export const Explorer = memo((props: ExplorerProps) => { /> ); })()} + {isNavigating && ( + + )}
Date: Thu, 15 Jan 2026 15:18:11 +0100 Subject: [PATCH 07/11] Tigny refactor --- .../_s3Next/s3ExplorerRootUiController/thunks.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts index fb1927022..dd932e321 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts @@ -49,12 +49,15 @@ export const thunks = { return { routeParams_toSet: routeParams }; } - const wrap = await dispatch( - s3ProfilesManagement.protectedThunks.getS3ConfigAndClientForExplorer() - ); + const { s3Profile } = + (await dispatch( + s3ProfilesManagement.protectedThunks.getS3ConfigAndClientForExplorer() + )) ?? {}; + + console.log(s3Profile); const routeParams_toSet: RouteParams = { - profile: wrap === undefined ? undefined : wrap.s3Profile.id, + profile: s3Profile === undefined ? undefined : s3Profile.id, path: "" }; From 9746030c829126c8a1407ac47e8fa4074bc39229 Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 19 Jan 2026 21:31:12 +0100 Subject: [PATCH 08/11] Move from profileId to profileName --- web/src/core/adapters/onyxiaApi/ApiTypes.ts | 4 +- web/src/core/adapters/onyxiaApi/onyxiaApi.ts | 7 +- web/src/core/bootstrap.ts | 2 +- .../core/ports/OnyxiaApi/DeploymentRegion.ts | 4 +- .../s3ExplorerRootUiController/selectors.ts | 20 +-- .../s3ExplorerRootUiController/state.ts | 6 +- .../s3ExplorerRootUiController/thunks.ts | 27 ++-- .../selectors.ts | 133 +++++++++++++----- .../s3ProfilesCreationUiController/state.ts | 15 +- .../s3ProfilesCreationUiController/thunks.ts | 35 +++-- .../resolveTemplatedBookmark.ts | 8 +- .../decoupledLogic/resolveTemplatedStsRole.ts | 7 +- .../decoupledLogic/s3Profiles.ts | 73 +++++++--- ...DefaultS3ProfilesAfterPotentialDeletion.ts | 38 +++-- .../decoupledLogic/userConfigsS3Bookmarks.ts | 49 ++++++- .../_s3Next/s3ProfilesManagement/thunks.ts | 117 +++++++++------ .../decoupledLogic/ProjectConfigs.ts | 3 + .../core/usecases/projectManagement/thunks.ts | 15 +- 18 files changed, 370 insertions(+), 193 deletions(-) diff --git a/web/src/core/adapters/onyxiaApi/ApiTypes.ts b/web/src/core/adapters/onyxiaApi/ApiTypes.ts index 6375eb3c5..b6b982ab0 100644 --- a/web/src/core/adapters/onyxiaApi/ApiTypes.ts +++ b/web/src/core/adapters/onyxiaApi/ApiTypes.ts @@ -83,6 +83,7 @@ export type ApiTypes = { }; data?: { S3?: ArrayOrNot<{ + profileName?: string; URL: string; pathStyleAccess?: true; @@ -94,6 +95,7 @@ export type ApiTypes = { { roleARN: string; roleSessionName: string; + profileName: string; } & ( | { claimName?: undefined } | { @@ -125,7 +127,7 @@ export type ApiTypes = { title: LocalizedString; description?: LocalizedString; tags?: LocalizedString[]; - forStsRoleSessionName?: string | string[]; + forProfileName?: string | string[]; } & ( | { claimName?: undefined } | { diff --git a/web/src/core/adapters/onyxiaApi/onyxiaApi.ts b/web/src/core/adapters/onyxiaApi/onyxiaApi.ts index aa4f3a08c..4cfdaf770 100644 --- a/web/src/core/adapters/onyxiaApi/onyxiaApi.ts +++ b/web/src/core/adapters/onyxiaApi/onyxiaApi.ts @@ -377,6 +377,7 @@ export function createOnyxiaApi(params: { s3Config_api ): DeploymentRegion.S3Next.S3Profile => { return { + profileName: s3Config_api.profileName, url: s3Config_api.URL, pathStyleAccess: s3Config_api.pathStyleAccess ?? @@ -413,6 +414,8 @@ export function createOnyxiaApi(params: { role_api.roleARN, roleSessionName: role_api.roleSessionName, + profileName: + role_api.profileName, ...(role_api.claimName === undefined ? { @@ -462,10 +465,10 @@ export function createOnyxiaApi(params: { tags: bookmarkedDirectory_api.tags ?? [], - forStsRoleSessionNames: + forProfileNames: (() => { const v = - bookmarkedDirectory_api.forStsRoleSessionName; + bookmarkedDirectory_api.forProfileName; if ( v === diff --git a/web/src/core/bootstrap.ts b/web/src/core/bootstrap.ts index 8ad19f5a8..bfb666fcb 100644 --- a/web/src/core/bootstrap.ts +++ b/web/src/core/bootstrap.ts @@ -157,7 +157,7 @@ export async function bootstrapCore( } const result = await dispatch( - usecases.s3ProfilesManagement.protectedThunks.getS3ConfigAndClientForExplorer() + usecases.s3ProfilesManagement.protectedThunks.getS3ProfileAndClientForExplorer() ); if (result === undefined) { diff --git a/web/src/core/ports/OnyxiaApi/DeploymentRegion.ts b/web/src/core/ports/OnyxiaApi/DeploymentRegion.ts index 5463f65c4..b3d4e6d27 100644 --- a/web/src/core/ports/OnyxiaApi/DeploymentRegion.ts +++ b/web/src/core/ports/OnyxiaApi/DeploymentRegion.ts @@ -174,6 +174,7 @@ export namespace DeploymentRegion { export namespace S3Next { /** https://github.com/InseeFrLab/onyxia-api/blob/main/docs/region-configuration.md#s3 */ export type S3Profile = { + profileName: string | undefined; url: string; pathStyleAccess: boolean; region: string | undefined; @@ -190,6 +191,7 @@ export namespace DeploymentRegion { export type StsRole = { roleARN: string; roleSessionName: string; + profileName: string; } & ( | { claimName: undefined; @@ -208,7 +210,7 @@ export namespace DeploymentRegion { title: LocalizedString; description: LocalizedString | undefined; tags: LocalizedString[]; - forStsRoleSessionNames: string[]; + forProfileNames: string[]; } & ( | { claimName: undefined; diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts index eef07a424..b78c0ac44 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts @@ -18,10 +18,9 @@ export const protectedSelectors = { }; export type View = { - selectedS3ProfileId: string | undefined; - selectedS3Profile_creationTime: number | undefined; + selectedProfileName: string | undefined; availableS3Profiles: { - id: string; + profileName: string; displayName: string; }[]; bookmarks: { @@ -48,8 +47,7 @@ const view = createSelector( if (routeParams.profile === undefined) { return { - selectedS3ProfileId: undefined, - selectedS3Profile_creationTime: undefined, + selectedProfileName: undefined, availableS3Profiles: [], bookmarks: [], s3UriPrefixObj: undefined, @@ -59,10 +57,10 @@ const view = createSelector( }; } - const selectedS3ProfileId = routeParams.profile; + const profileName = routeParams.profile; const s3Profile = s3Profiles.find( - s3Profile => s3Profile.id === selectedS3ProfileId + s3Profile => s3Profile.profileName === profileName ); // NOTE: We enforce this invariant while loading the route @@ -77,13 +75,9 @@ const view = createSelector( }); return { - selectedS3ProfileId, - selectedS3Profile_creationTime: - s3Profile.origin !== "created by user (or group project member)" - ? undefined - : s3Profile.creationTime, + selectedProfileName: profileName, availableS3Profiles: s3Profiles.map(s3Profile => ({ - id: s3Profile.id, + profileName: s3Profile.profileName, displayName: s3Profile.paramsOfCreateS3Client.url })), bookmarks: s3Profile.bookmarks, diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/state.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/state.ts index 4fa0b41a6..a2e2e8de1 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/state.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/state.ts @@ -44,11 +44,11 @@ export const { actions, reducer } = createUsecaseActions({ }, selectedS3ProfileUpdated: ( state, - { payload }: { payload: { s3ProfileId: string } } + { payload }: { payload: { profileName: string } } ) => { - const { s3ProfileId } = payload; + const { profileName } = payload; - state.routeParams.profile = s3ProfileId; + state.routeParams.profile = profileName; state.routeParams.path = ""; } } diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts index dd932e321..625a10097 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts @@ -16,15 +16,16 @@ export const thunks = { const { routeParams } = params; if (routeParams.profile !== undefined) { - const s3ProfileId = routeParams.profile; + const profileName = routeParams.profile; { const s3Profiles = s3ProfilesManagement.selectors.s3Profiles(getState()); if ( - s3Profiles.find(s3Profile => s3Profile.id === s3ProfileId) === - undefined + s3Profiles.find( + s3Profile => s3Profile.profileName === profileName + ) === undefined ) { return dispatch(thunks.load({ routeParams: { path: "" } })); } @@ -32,7 +33,7 @@ export const thunks = { await dispatch( s3ProfilesManagement.protectedThunks.changeIsDefault({ - s3ProfileId, + profileName, usecase: "explorer", value: true }) @@ -51,13 +52,13 @@ export const thunks = { const { s3Profile } = (await dispatch( - s3ProfilesManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfilesManagement.protectedThunks.getS3ProfileAndClientForExplorer() )) ?? {}; console.log(s3Profile); const routeParams_toSet: RouteParams = { - profile: s3Profile === undefined ? undefined : s3Profile.id, + profile: s3Profile === undefined ? undefined : s3Profile.profileName, path: "" }; @@ -90,15 +91,15 @@ export const thunks = { dispatch(actions.s3UrlUpdated({ s3UriPrefixObj })); }, updateSelectedS3Profile: - (params: { s3ProfileId: string }) => + (params: { profileName: string }) => async (...args) => { const [dispatch] = args; - const { s3ProfileId } = params; + const { profileName } = params; await dispatch( s3ProfilesManagement.protectedThunks.changeIsDefault({ - s3ProfileId, + profileName, usecase: "explorer", value: true }) @@ -106,7 +107,7 @@ export const thunks = { dispatch( actions.selectedS3ProfileUpdated({ - s3ProfileId + profileName }) ); }, @@ -123,15 +124,15 @@ export const thunks = { const [dispatch, getState] = args; - const { selectedS3ProfileId, s3UriPrefixObj, bookmarkStatus } = + const { selectedProfileName, s3UriPrefixObj, bookmarkStatus } = selectors.view(getState()); - assert(selectedS3ProfileId !== undefined); + assert(selectedProfileName !== undefined); assert(s3UriPrefixObj !== undefined); await dispatch( s3ProfilesManagement.protectedThunks.createDeleteOrUpdateBookmark({ - s3ProfileId: selectedS3ProfileId, + profileName: selectedProfileName, s3UriPrefixObj, action: bookmarkStatus.isBookmarked ? { diff --git a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts index 39a22c86a..4d0628ef1 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts @@ -6,6 +6,7 @@ import { assert } from "tsafe/assert"; import { id } from "tsafe/id"; import type { ProjectConfigs } from "core/usecases/projectManagement"; import type { ParamsOfCreateS3Client } from "core/adapters/s3Client"; +import * as s3ProfilesManagement from "core/usecases/_s3Next/s3ProfilesManagement"; const readyState = (rootState: RootState) => { const state = rootState[name]; @@ -27,55 +28,111 @@ const formValues = createSelector(readyState, state => { return state.formValues; }); -const formValuesErrors = createSelector(formValues, formValues => { - if (formValues === null) { - return null; - } +const existingProfileNames = createSelector( + isReady, + createSelector(readyState, state => { + if (state === null) { + return null; + } + return state.creationTimeOfProfileToEdit; + }), + s3ProfilesManagement.selectors.s3Profiles, + (isReady, creationTimeOfProfileToEdit, s3Profiles) => { + if (!isReady) { + return null; + } - const out: Record< - keyof typeof formValues, - "must be an url" | "is required" | "not a valid access key id" | undefined - > = {} as any; - - for (const key of objectKeys(formValues)) { - out[key] = (() => { - required_fields: { - if ( - !( - key === "url" || - key === "friendlyName" || - (!formValues.isAnonymous && - (key === "accessKeyId" || key === "secretAccessKey")) - ) - ) { - break required_fields; + assert(creationTimeOfProfileToEdit !== null); + + return s3Profiles + .filter(s3Profile => { + if (creationTimeOfProfileToEdit === undefined) { + return true; } - const value = formValues[key]; + if (s3Profile.origin !== "created by user (or group project member)") { + return true; + } - if ((value ?? "").trim() !== "") { - break required_fields; + if (s3Profile.creationTime === creationTimeOfProfileToEdit) { + return false; } - return "is required"; - } + return true; + }) + .map(s3Profile => s3Profile.profileName); + } +); - if (key === "url") { - const value = formValues[key]; +const formValuesErrors = createSelector( + isReady, + formValues, + existingProfileNames, + (isReady, formValues, existingProfileNames) => { + if (!isReady) { + return null; + } - try { - new URL(value.startsWith("http") ? value : `https://${value}`); - } catch { - return "must be an url"; + assert(formValues !== null); + assert(existingProfileNames !== null); + + const out: Record< + keyof typeof formValues, + | "must be an url" + | "is required" + | "not a valid access key id" + | "profile name already used" + | undefined + > = {} as any; + + for (const key of objectKeys(formValues)) { + out[key] = (() => { + required_fields: { + if ( + !( + key === "url" || + key === "profileName" || + (!formValues.isAnonymous && + (key === "accessKeyId" || key === "secretAccessKey")) + ) + ) { + break required_fields; + } + + const value = formValues[key]; + + if ((value ?? "").trim() !== "") { + break required_fields; + } + + return "is required"; } - } - return undefined; - })(); - } + if (key === "url") { + const value = formValues[key]; - return out; -}); + try { + new URL(value.startsWith("http") ? value : `https://${value}`); + } catch { + return "must be an url"; + } + } + + if (key === "profileName") { + const value = formValues[key]; + + if (existingProfileNames.includes(value)) { + return "profile name already used"; + } + } + + return undefined; + })(); + } + + return out; + } +); const isFormSubmittable = createSelector( isReady, diff --git a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/state.ts b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/state.ts index 969ea6d16..98aaed094 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/state.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/state.ts @@ -12,13 +12,12 @@ export namespace State { export type Ready = { stateDescription: "ready"; formValues: Ready.FormValues; - s3ProfileCreationTime: number; - action: "Update existing S3 profile" | "Create new S3 profile"; + creationTimeOfProfileToEdit: number | undefined; }; export namespace Ready { export type FormValues = { - friendlyName: string; + profileName: string; url: string; region: string | undefined; pathStyleAccess: boolean; @@ -53,21 +52,17 @@ export const { reducer, actions } = createUsecaseActions({ payload }: { payload: { - creationTimeOfS3ProfileToEdit: number | undefined; + creationTimeOfProfileToEdit: number | undefined; initialFormValues: State.Ready["formValues"]; }; } ) => { - const { creationTimeOfS3ProfileToEdit, initialFormValues } = payload; + const { creationTimeOfProfileToEdit, initialFormValues } = payload; return id({ stateDescription: "ready", formValues: initialFormValues, - s3ProfileCreationTime: creationTimeOfS3ProfileToEdit ?? Date.now(), - action: - creationTimeOfS3ProfileToEdit === undefined - ? "Create new S3 profile" - : "Update existing S3 profile" + creationTimeOfProfileToEdit }); }, formValueChanged: ( diff --git a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts index 214f838c8..c3914c349 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts @@ -7,37 +7,34 @@ import * as deploymentRegionManagement from "core/usecases/deploymentRegionManag export const thunks = { initialize: - (params: { creationTimeOfS3ProfileToEdit: number | undefined }) => + (params: { + // NOTE: Undefined for creation + profileName_toUpdate: string | undefined; + }) => async (...args) => { - const { creationTimeOfS3ProfileToEdit } = params; + const { profileName_toUpdate } = params; const [dispatch, getState] = args; const s3Profiles = s3ProfilesManagement.selectors.s3Profiles(getState()); update_existing_config: { - if (creationTimeOfS3ProfileToEdit === undefined) { + if (profileName_toUpdate === undefined) { break update_existing_config; } - const s3Profile = s3Profiles - .filter( - s3Profile => - s3Profile.origin === - "created by user (or group project member)" - ) - .find( - s3Profile => - s3Profile.creationTime === creationTimeOfS3ProfileToEdit - ); + const s3Profile = s3Profiles.find( + s3Profile => s3Profile.profileName === profileName_toUpdate + ); assert(s3Profile !== undefined); + assert(s3Profile.origin === "created by user (or group project member)"); dispatch( actions.initialized({ - creationTimeOfS3ProfileToEdit, + creationTimeOfProfileToEdit: s3Profile.creationTime, initialFormValues: { - friendlyName: s3Profile.friendlyName, + profileName: s3Profile.profileName, url: s3Profile.paramsOfCreateS3Client.url, region: s3Profile.paramsOfCreateS3Client.region, pathStyleAccess: @@ -83,9 +80,9 @@ export const thunks = { if (s3Profiles_defaultValuesOfCreationForm === undefined) { dispatch( actions.initialized({ - creationTimeOfS3ProfileToEdit: undefined, + creationTimeOfProfileToEdit: undefined, initialFormValues: { - friendlyName: "", + profileName: "", url: "", region: undefined, pathStyleAccess: false, @@ -101,9 +98,9 @@ export const thunks = { dispatch( actions.initialized({ - creationTimeOfS3ProfileToEdit: undefined, + creationTimeOfProfileToEdit: undefined, initialFormValues: { - friendlyName: "", + profileName: "", url: s3Profiles_defaultValuesOfCreationForm.url, region: s3Profiles_defaultValuesOfCreationForm.region, pathStyleAccess: diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedBookmark.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedBookmark.ts index 85fa029dc..8b1f7f22e 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedBookmark.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedBookmark.ts @@ -10,7 +10,7 @@ export type ResolvedTemplateBookmark = { description: LocalizedString | undefined; tags: LocalizedString[]; s3UriPrefixObj: S3UriPrefixObj; - forStsRoleSessionNames: string[]; + forProfileNames: string[]; }; export async function resolveTemplatedBookmark(params: { @@ -29,7 +29,7 @@ export async function resolveTemplatedBookmark(params: { title: bookmark_region.title, description: bookmark_region.description, tags: bookmark_region.tags, - forStsRoleSessionNames: bookmark_region.forStsRoleSessionNames + forProfileNames: bookmark_region.forProfileNames }) ]; } @@ -130,8 +130,8 @@ export async function resolveTemplatedBookmark(params: { ? undefined : substituteLocalizedString(bookmark_region.description), tags: bookmark_region.tags.map(tag => substituteLocalizedString(tag)), - forStsRoleSessionNames: bookmark_region.forStsRoleSessionNames.map( - stsRoleSessionName => substituteTemplateString(stsRoleSessionName) + forProfileNames: bookmark_region.forProfileNames.map(profileName => + substituteTemplateString(profileName) ) }); }) diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedStsRole.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedStsRole.ts index 0e8186a07..384e0fe48 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedStsRole.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/resolveTemplatedStsRole.ts @@ -6,6 +6,7 @@ import { getValueAtPath } from "core/tools/Stringifyable"; export type ResolvedTemplateStsRole = { roleARN: string; roleSessionName: string; + profileName: string; }; export async function resolveTemplatedStsRole(params: { @@ -18,7 +19,8 @@ export async function resolveTemplatedStsRole(params: { return [ id({ roleARN: stsRole_region.roleARN, - roleSessionName: stsRole_region.roleSessionName + roleSessionName: stsRole_region.roleSessionName, + profileName: stsRole_region.profileName }) ]; } @@ -97,7 +99,8 @@ export async function resolveTemplatedStsRole(params: { return id({ roleARN: substituteTemplateString(stsRole_region.roleARN), - roleSessionName: substituteTemplateString(stsRole_region.roleSessionName) + roleSessionName: substituteTemplateString(stsRole_region.roleSessionName), + profileName: substituteTemplateString(stsRole_region.profileName) }); }) .filter(x => x !== undefined); diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts index 3a6a8a714..3e884c7ee 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts @@ -1,7 +1,6 @@ import * as projectManagement from "core/usecases/projectManagement"; import type { DeploymentRegion } from "core/ports/OnyxiaApi/DeploymentRegion"; import type { ParamsOfCreateS3Client } from "core/adapters/s3Client"; -import { fnv1aHashToHex } from "core/tools/fnv1aHashToHex"; import { assert, type Equals } from "tsafe"; import type { LocalizedString } from "core/ports/OnyxiaApi"; import type { ResolvedTemplateBookmark } from "./resolveTemplatedBookmark"; @@ -13,7 +12,7 @@ export type S3Profile = S3Profile.DefinedInRegion | S3Profile.CreatedByUser; export namespace S3Profile { type Common = { - id: string; + profileName: string; isXOnyxiaDefault: boolean; isExplorerConfig: boolean; bookmarks: Bookmark[]; @@ -28,7 +27,6 @@ export namespace S3Profile { origin: "created by user (or group project member)"; creationTime: number; paramsOfCreateS3Client: ParamsOfCreateS3Client.NoSts; - friendlyName: string; }; export type Bookmark = { @@ -80,9 +78,8 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { return { origin: "created by user (or group project member)", + profileName: c.friendlyName, creationTime: c.creationTime, - friendlyName: c.friendlyName, - id: `${c.creationTime}`, paramsOfCreateS3Client, isXOnyxiaDefault: false, isExplorerConfig: false, @@ -129,17 +126,22 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { role: resolvedTemplatedStsRole }; - const id = `region-${fnv1aHashToHex( - JSON.stringify([c.url, c.sts.oidcParams.clientId ?? ""]) - )}`; + const profileName = (() => { + if (resolvedTemplatedStsRole === undefined) { + assert(c.profileName !== undefined); + return c.profileName; + } + + return resolvedTemplatedStsRole.profileName; + })(); return { origin: "defined in region", - id, + profileName, bookmarks: [ ...resolvedTemplatedBookmarks_forThisProfile - .filter(({ forStsRoleSessionNames }) => { - if (forStsRoleSessionNames.length === 0) { + .filter(({ forProfileNames }) => { + if (forProfileNames.length === 0) { return true; } @@ -166,13 +168,12 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { ); }; - return forStsRoleSessionNames.some( - stsRoleSessionName => - getDoMatch({ - stringWithWildcards: stsRoleSessionName, - candidate: - resolvedTemplatedStsRole.roleSessionName - }) + return forProfileNames.some(profileName => + getDoMatch({ + stringWithWildcards: profileName, + candidate: + resolvedTemplatedStsRole.profileName + }) ); }) .map(({ title, s3UriPrefixObj }) => ({ @@ -184,10 +185,10 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { userConfigs_s3BookmarksStr: fromVault.userConfigs_s3BookmarksStr }) - .filter(entry => entry.s3ProfileId === id) + .filter(entry => entry.profileName === profileName) .map(entry => ({ isReadonly: false, - displayName: entry.displayName, + displayName: entry.displayName ?? undefined, s3UriPrefixObj: entry.s3UriPrefixObj })) ], @@ -223,18 +224,44 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { .flat() ]; + for (const s3Profile of [...s3Profiles].sort((a, b) => { + if (a.origin === b.origin) { + return 0; + } + + return a.origin === "defined in region" ? -1 : 1; + })) { + const s3Profiles_conflicting = s3Profiles.filter( + s3Profile_i => + s3Profile_i !== s3Profile && + s3Profile_i.profileName === s3Profile.profileName + ); + + if (s3Profiles_conflicting.length === 0) { + continue; + } + + console.warn(`The is more than one s3Profile named: ${s3Profile.profileName}`); + + for (const s3Profile_conflicting of s3Profiles_conflicting) { + const i = s3Profiles.indexOf(s3Profile_conflicting); + + s3Profiles.splice(i, 1); + } + } + ( [ ["defaultXOnyxia", fromVault.projectConfigs_s3.s3ConfigId_defaultXOnyxia], ["explorer", fromVault.projectConfigs_s3.s3ConfigId_explorer] ] as const - ).forEach(([prop, s3ProfileId]) => { - if (s3ProfileId === undefined) { + ).forEach(([prop, profileName]) => { + if (profileName === undefined) { return; } const s3Profile = - s3Profiles.find(({ id }) => id === s3ProfileId) ?? + s3Profiles.find(s3Profile => s3Profile.profileName === profileName) ?? s3Profiles.find(s3Config => s3Config.origin === "defined in region"); if (s3Profile === undefined) { diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts index a40203cfa..49480fb09 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts @@ -3,13 +3,13 @@ import type { DeploymentRegion } from "core/ports/OnyxiaApi/DeploymentRegion"; import { aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet } from "./s3Profiles"; type R = Record< - "s3ConfigId_defaultXOnyxia" | "s3ConfigId_explorer", + "profileName_defaultXOnyxia" | "profileName_explorer", | { isUpdateNeeded: false; } | { isUpdateNeeded: true; - s3ProfileId: string | undefined; + profileName: string | undefined; } >; @@ -34,35 +34,47 @@ export function updateDefaultS3ProfilesAfterPotentialDeletion(params: { }); const actions: R = { - s3ConfigId_defaultXOnyxia: { + profileName_defaultXOnyxia: { isUpdateNeeded: false }, - s3ConfigId_explorer: { + profileName_explorer: { isUpdateNeeded: false } }; for (const propertyName of [ - "s3ConfigId_defaultXOnyxia", - "s3ConfigId_explorer" + "s3ConfigId_defaultXOnyxia", // TODO: Rename + "s3ConfigId_explorer" // TODO: Rename ] as const) { - const s3ConfigId_default = fromVault.projectConfigs_s3[propertyName]; + const profileName_default = fromVault.projectConfigs_s3[propertyName]; - if (s3ConfigId_default === undefined) { + if (profileName_default === undefined) { continue; } - if (s3Profiles.find(({ id }) => id === s3ConfigId_default) !== undefined) { + if ( + s3Profiles.find(({ profileName }) => profileName === profileName_default) !== + undefined + ) { continue; } - const s3ConfigId_toUseAsDefault = s3Profiles.find( + const profileName_newDefault = s3Profiles.find( ({ origin }) => origin === "defined in region" - )?.id; + )?.profileName; - actions[propertyName] = { + actions[ + (() => { + switch (propertyName) { + case "s3ConfigId_defaultXOnyxia": + return "profileName_defaultXOnyxia"; + case "s3ConfigId_explorer": + return "profileName_explorer"; + } + })() + ] = { isUpdateNeeded: true, - s3ProfileId: s3ConfigId_toUseAsDefault + profileName: profileName_newDefault }; } diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/userConfigsS3Bookmarks.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/userConfigsS3Bookmarks.ts index 392b66b34..7a5c9495d 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/userConfigsS3Bookmarks.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/userConfigsS3Bookmarks.ts @@ -1,11 +1,44 @@ import type { S3UriPrefixObj } from "core/tools/S3Uri"; +import { z } from "zod"; +import { assert, type Equals, id, is } from "tsafe"; export type UserProfileS3Bookmark = { - s3ProfileId: string; - displayName: string | undefined; + profileName: string; + displayName: string | null; s3UriPrefixObj: S3UriPrefixObj; }; +const zS3UriPrefixObj = (() => { + type TargetType = S3UriPrefixObj; + + const zTargetType = z.object({ + bucket: z.string(), + keyPrefix: z.string() + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); +})(); + +const zUserProfileS3Bookmark = (() => { + type TargetType = UserProfileS3Bookmark; + + const zTargetType = z.object({ + profileName: z.string(), + displayName: z.union([z.string(), z.null()]), + s3UriPrefixObj: zS3UriPrefixObj + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); +})(); + export function parseUserConfigsS3BookmarksStr(params: { userConfigs_s3BookmarksStr: string | null; }): UserProfileS3Bookmark[] { @@ -15,7 +48,17 @@ export function parseUserConfigsS3BookmarksStr(params: { return []; } - return JSON.parse(userConfigs_s3BookmarksStr); + const userProfileS3Bookmarks: unknown = JSON.parse(userConfigs_s3BookmarksStr); + + try { + z.array(zUserProfileS3Bookmark).parse(userProfileS3Bookmarks); + } catch { + return []; + } + + assert(is(userProfileS3Bookmarks)); + + return userProfileS3Bookmarks; } export function serializeUserConfigsS3Bookmarks(params: { diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/thunks.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/thunks.ts index 8d9bdbb82..a3b48f5c3 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/thunks.ts @@ -3,7 +3,6 @@ import { selectors, protectedSelectors } from "./selectors"; import * as projectManagement from "core/usecases/projectManagement"; import { assert } from "tsafe/assert"; import type { S3Client } from "core/ports/S3Client"; -import { createUsecaseContextApi } from "clean-architecture"; import { updateDefaultS3ProfilesAfterPotentialDeletion } from "./decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion"; import structuredClone from "@ungap/structured-clone"; import * as deploymentRegionManagement from "core/usecases/deploymentRegionManagement"; @@ -22,10 +21,10 @@ import { import * as userConfigs from "core/usecases/userConfigs"; export const thunks = { - deleteS3Config: - (params: { s3ProfileCreationTime: number }) => + deleteS3Profile: + (params: { profileName: string }) => async (...args) => { - const { s3ProfileCreationTime } = params; + const { profileName } = params; const [dispatch, getState] = args; @@ -34,7 +33,7 @@ export const thunks = { ); const i = projectConfigs_s3.s3Configs.findIndex( - ({ creationTime }) => creationTime === s3ProfileCreationTime + s3Profile => s3Profile.friendlyName === profileName ); assert(i !== -1); @@ -54,19 +53,27 @@ export const thunks = { } }); - await Promise.all( - (["s3ConfigId_defaultXOnyxia", "s3ConfigId_explorer"] as const).map( - async propertyName => { - const action = actions[propertyName]; + for (const propertyName of [ + "profileName_defaultXOnyxia", + "profileName_explorer" + ] as const) { + const action = actions[propertyName]; - if (!action.isUpdateNeeded) { - return; - } + if (!action.isUpdateNeeded) { + continue; + } - projectConfigs_s3[propertyName] = action.s3ProfileId; - } - ) - ); + projectConfigs_s3[ + (() => { + switch (propertyName) { + case "profileName_defaultXOnyxia": + return "s3ConfigId_defaultXOnyxia"; + case "profileName_explorer": + return "s3ConfigId_explorer"; + } + })() + ] = action.profileName; + } } await dispatch( @@ -78,21 +85,22 @@ export const thunks = { } } satisfies Thunks; +const globalContext = { + prS3ClientByProfileName: new Map>() +}; + export const protectedThunks = { - getS3ClientForSpecificConfig: - (params: { s3ProfileId: string | undefined }) => + getS3Client: + (params: { profileName: string }) => async (...args): Promise => { - const { s3ProfileId } = params; + const { profileName } = params; const [, getState, rootContext] = args; - const { prS3ClientByConfigId: prS3ClientByProfileId } = - getContext(rootContext); - const s3Profile = (() => { const s3Profiles = selectors.s3Profiles(getState()); const s3Config = s3Profiles.find( - s3Profile => s3Profile.id === s3ProfileId + s3Profile => s3Profile.profileName === profileName ); assert(s3Config !== undefined); @@ -100,7 +108,9 @@ export const protectedThunks = { })(); use_cached_s3Client: { - const prS3Client = prS3ClientByProfileId.get(s3Profile.id); + const prS3Client = globalContext.prS3ClientByProfileName.get( + s3Profile.profileName + ); if (prS3Client === undefined) { break use_cached_s3Client; @@ -189,11 +199,11 @@ export const protectedThunks = { ); })(); - prS3ClientByProfileId.set(s3Profile.id, prS3Client); + globalContext.prS3ClientByProfileName.set(s3Profile.profileName, prS3Client); return prS3Client; }, - getS3ConfigAndClientForExplorer: + getS3ProfileAndClientForExplorer: () => async ( ...args @@ -209,8 +219,8 @@ export const protectedThunks = { } const s3Client = await dispatch( - protectedThunks.getS3ClientForSpecificConfig({ - s3ProfileId: s3Profile.id + protectedThunks.getS3Client({ + profileName: s3Profile.profileName }) ); @@ -219,7 +229,7 @@ export const protectedThunks = { createOrUpdateS3Profile: (params: { s3Config_vault: projectManagement.ProjectConfigs.S3Config }) => async (...args) => { - const { s3Config_vault: s3Config_vault } = params; + const { s3Config_vault } = params; const [dispatch, getState] = args; @@ -232,6 +242,27 @@ export const protectedThunks = { projectS3Config_i.creationTime === s3Config_vault.creationTime ); + update_default_selected: { + if (i === -1) { + break update_default_selected; + } + + const s3Config_vault_current = fromVault.s3Configs[i]; + + if (s3Config_vault.friendlyName === s3Config_vault.friendlyName) { + break update_default_selected; + } + + for (const propertyName of [ + "s3ConfigId_defaultXOnyxia", + "s3ConfigId_explorer" + ] as const) { + if (fromVault[propertyName] === s3Config_vault_current.friendlyName) { + fromVault[propertyName] = s3Config_vault.friendlyName; + } + } + } + if (i < 0) { fromVault.s3Configs.push(s3Config_vault); } else { @@ -247,7 +278,7 @@ export const protectedThunks = { }, createDeleteOrUpdateBookmark: (params: { - s3ProfileId: string; + profileName: string; s3UriPrefixObj: S3UriPrefixObj; action: | { @@ -259,13 +290,15 @@ export const protectedThunks = { }; }) => async (...args) => { - const { s3ProfileId, s3UriPrefixObj, action } = params; + const { profileName, s3UriPrefixObj, action } = params; const [dispatch, getState] = args; const s3Profiles = selectors.s3Profiles(getState()); - const s3Profile = s3Profiles.find(s3Profile => s3Profile.id === s3ProfileId); + const s3Profile = s3Profiles.find( + s3Profile => s3Profile.profileName === profileName + ); assert(s3Profile !== undefined); @@ -332,7 +365,7 @@ export const protectedThunks = { const index = userConfigs_s3Bookmarks.findIndex( entry => - entry.s3ProfileId === s3Profile.id && + entry.profileName === s3Profile.profileName && same(entry.s3UriPrefixObj, s3UriPrefixObj) ); @@ -340,8 +373,8 @@ export const protectedThunks = { case "create or update": { const bookmark_new = { - s3ProfileId: s3Profile.id, - displayName: action.displayName, + profileName: s3Profile.profileName, + displayName: action.displayName ?? null, s3UriPrefixObj }; @@ -375,12 +408,12 @@ export const protectedThunks = { }, changeIsDefault: (params: { - s3ProfileId: string; + profileName: string; usecase: "defaultXOnyxia" | "explorer"; value: boolean; }) => async (...args) => { - const { s3ProfileId, usecase, value } = params; + const { profileName, usecase, value } = params; const [dispatch, getState] = args; @@ -401,17 +434,17 @@ export const protectedThunks = { const s3ProfileId_currentDefault = fromVault[propertyName]; if (value) { - if (s3ProfileId_currentDefault === s3ProfileId) { + if (s3ProfileId_currentDefault === profileName) { return; } } else { - if (s3ProfileId_currentDefault !== s3ProfileId) { + if (s3ProfileId_currentDefault !== profileName) { return; } } } - fromVault[propertyName] = value ? s3ProfileId : undefined; + fromVault[propertyName] = value ? profileName : undefined; await dispatch( projectManagement.protectedThunks.updateConfigValue({ @@ -516,7 +549,3 @@ export const protectedThunks = { ); } } satisfies Thunks; - -const { getContext } = createUsecaseContextApi(() => ({ - prS3ClientByConfigId: new Map>() -})); diff --git a/web/src/core/usecases/projectManagement/decoupledLogic/ProjectConfigs.ts b/web/src/core/usecases/projectManagement/decoupledLogic/ProjectConfigs.ts index 7b0c151b9..9d19877b6 100644 --- a/web/src/core/usecases/projectManagement/decoupledLogic/ProjectConfigs.ts +++ b/web/src/core/usecases/projectManagement/decoupledLogic/ProjectConfigs.ts @@ -13,7 +13,9 @@ export type ProjectConfigs = { restorableConfigs: ProjectConfigs.RestorableServiceConfig[]; s3: { s3Configs: ProjectConfigs.S3Config[]; + // TODO: Rename to profileName_defaultXOnyxia s3ConfigId_defaultXOnyxia: string | undefined; + // TODO: Rename to profileName_explorer s3ConfigId_explorer: string | undefined; }; clusterNotificationCheckoutTime: number; @@ -22,6 +24,7 @@ export type ProjectConfigs = { export namespace ProjectConfigs { export type S3Config = { creationTime: number; + // TODO: Rename this to profileName friendlyName: string; url: string; region: string | undefined; diff --git a/web/src/core/usecases/projectManagement/thunks.ts b/web/src/core/usecases/projectManagement/thunks.ts index e61b18d42..11bedfedd 100644 --- a/web/src/core/usecases/projectManagement/thunks.ts +++ b/web/src/core/usecases/projectManagement/thunks.ts @@ -147,8 +147,8 @@ export const thunks = { let needUpdate = false; for (const propertyName of [ - "s3ConfigId_defaultXOnyxia", - "s3ConfigId_explorer" + "profileName_defaultXOnyxia", + "profileName_explorer" ] as const) { const action = actions[propertyName]; @@ -158,7 +158,16 @@ export const thunks = { needUpdate = true; - projectConfigs.s3[propertyName] = action.s3ProfileId; + projectConfigs.s3[ + (() => { + switch (propertyName) { + case "profileName_defaultXOnyxia": + return "s3ConfigId_defaultXOnyxia"; + case "profileName_explorer": + return "s3ConfigId_explorer"; + } + })() + ] = action.profileName; } if (!needUpdate) { From c842142445d768ff4b1b4af38533c13111f1b8fe Mon Sep 17 00:00:00 2001 From: garronej Date: Tue, 20 Jan 2026 14:20:33 +0100 Subject: [PATCH 09/11] Correctly restore bookmark on edit --- .../selectors.ts | 60 ++++++++++++++----- .../s3ProfilesCreationUiController/thunks.ts | 5 +- .../decoupledLogic/s3Profiles.ts | 1 - 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts index 4d0628ef1..56e6aa880 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/selectors.ts @@ -7,6 +7,7 @@ import { id } from "tsafe/id"; import type { ProjectConfigs } from "core/usecases/projectManagement"; import type { ParamsOfCreateS3Client } from "core/adapters/s3Client"; import * as s3ProfilesManagement from "core/usecases/_s3Next/s3ProfilesManagement"; +import * as projectManagement from "core/usecases/projectManagement"; const readyState = (rootState: RootState) => { const state = rootState[name]; @@ -180,14 +181,16 @@ const submittableFormValuesAsProjectS3Config = createSelector( if (state === null) { return null; } - return state.s3ProfileCreationTime; + return state.creationTimeOfProfileToEdit; }), + projectManagement.protectedSelectors.projectConfig, ( isReady, formValues, formattedFormValuesUrl, isFormSubmittable, - s3ProfileCreationTime + creationTimeOfProfileToEdit, + projectConfig ) => { if (!isReady) { return null; @@ -195,7 +198,7 @@ const submittableFormValuesAsProjectS3Config = createSelector( assert(formValues !== null); assert(formattedFormValuesUrl !== null); assert(isFormSubmittable !== null); - assert(s3ProfileCreationTime !== null); + assert(creationTimeOfProfileToEdit !== null); if (!isFormSubmittable) { return undefined; @@ -203,9 +206,30 @@ const submittableFormValuesAsProjectS3Config = createSelector( assert(formattedFormValuesUrl !== undefined); - return id({ - creationTime: s3ProfileCreationTime, - friendlyName: formValues.friendlyName.trim(), + const projectS3Config_current = (() => { + if (creationTimeOfProfileToEdit === undefined) { + return undefined; + } + + const projectS3Config_current = projectConfig.s3.s3Configs.find( + s3Config => s3Config.creationTime === creationTimeOfProfileToEdit + ); + + assert(projectS3Config_current !== undefined); + + return projectS3Config_current; + })(); + + return id< + Omit & { + creationTime: number | undefined; + } + >({ + creationTime: + projectS3Config_current === undefined + ? undefined + : projectS3Config_current.creationTime, + friendlyName: formValues.profileName.trim(), url: formattedFormValuesUrl, region: formValues.region?.trim(), pathStyleAccess: formValues.pathStyleAccess, @@ -224,8 +248,14 @@ const submittableFormValuesAsProjectS3Config = createSelector( }; })(), // TODO: Delete once we move on - workingDirectoryPath: "mybucket/my/prefix/", - bookmarks: [] + workingDirectoryPath: + projectS3Config_current === undefined + ? "mybucket/my/prefix/" + : projectS3Config_current.workingDirectoryPath, + bookmarks: + projectS3Config_current === undefined + ? [] + : projectS3Config_current.bookmarks }); } ); @@ -285,20 +315,18 @@ const urlStylesExamples = createSelector( } ); -const isEditionOfAnExistingConfig = createSelector(readyState, state => { - if (state === null) { - return null; - } - return state.action === "Update existing S3 profile"; -}); - const main = createSelector( isReady, formValues, formValuesErrors, isFormSubmittable, urlStylesExamples, - isEditionOfAnExistingConfig, + createSelector(readyState, state => { + if (state === null) { + return null; + } + return state.creationTimeOfProfileToEdit !== undefined; + }), ( isReady, formValues, diff --git a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts index c3914c349..796842440 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesCreationUiController/thunks.ts @@ -134,7 +134,10 @@ export const thunks = { await dispatch( s3ProfilesManagement.protectedThunks.createOrUpdateS3Profile({ - s3Config_vault + s3Config_vault: { + ...s3Config_vault, + creationTime: s3Config_vault.creationTime ?? Date.now() + } }) ); diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts index 3e884c7ee..fd773d289 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/s3Profiles.ts @@ -83,7 +83,6 @@ export function aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet(params: { paramsOfCreateS3Client, isXOnyxiaDefault: false, isExplorerConfig: false, - // TODO: Actually store custom bookmarks bookmarks: (c.bookmarks ?? []).map( ({ displayName, s3UriPrefixObj }) => ({ displayName, From dce2ad85dedf274f36f77800dbe631f5af8bf968 Mon Sep 17 00:00:00 2001 From: garronej Date: Thu, 22 Jan 2026 15:40:48 +0100 Subject: [PATCH 10/11] Move from profileId to profileName (follow up) --- .../s3ExplorerRootUiController/selectors.ts | 22 ++- .../s3ExplorerRootUiController/thunks.ts | 8 +- web/src/core/usecases/fileExplorer/thunks.ts | 18 +-- web/src/core/usecases/launcher/selectors.ts | 30 ++-- web/src/core/usecases/launcher/state.ts | 4 +- web/src/core/usecases/launcher/thunks.ts | 85 ++++++----- .../core/usecases/userProfileForm/thunks.ts | 2 +- web/src/ui/i18n/resources/de.tsx | 55 +++++++ web/src/ui/i18n/resources/en.tsx | 50 +++++++ web/src/ui/i18n/resources/es.tsx | 53 +++++++ web/src/ui/i18n/resources/fi.tsx | 54 +++++++ web/src/ui/i18n/resources/fr.tsx | 55 +++++++ web/src/ui/i18n/resources/it.tsx | 59 +++++++- web/src/ui/i18n/resources/nl.tsx | 52 +++++++ web/src/ui/i18n/resources/no.tsx | 53 +++++++ web/src/ui/i18n/resources/zh-CN.tsx | 49 +++++++ web/src/ui/i18n/types.ts | 1 + .../ui/pages/launcher/LauncherMainCard.tsx | 17 +-- web/src/ui/pages/s3Explorer/Page.tsx | 65 +++++---- ...og.tsx => CreateOrUpdateProfileDialog.tsx} | 134 +++++++++--------- .../S3ConfigDialogs/S3ConfigDialogs.tsx | 16 ++- 21 files changed, 672 insertions(+), 210 deletions(-) rename web/src/ui/pages/s3Explorer/S3ConfigDialogs/{AddCustomS3ConfigDialog.tsx => CreateOrUpdateProfileDialog.tsx} (82%) diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts index b78c0ac44..39fc1593c 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/selectors.ts @@ -18,11 +18,9 @@ export const protectedSelectors = { }; export type View = { - selectedProfileName: string | undefined; - availableS3Profiles: { - profileName: string; - displayName: string; - }[]; + selectedS3ProfileName: string | undefined; + isSelectedS3ProfileEditable: boolean; + availableS3ProfileNames: string[]; bookmarks: { displayName: LocalizedString | undefined; s3UriPrefixObj: S3UriPrefixObj; @@ -47,8 +45,9 @@ const view = createSelector( if (routeParams.profile === undefined) { return { - selectedProfileName: undefined, - availableS3Profiles: [], + selectedS3ProfileName: undefined, + isSelectedS3ProfileEditable: false, + availableS3ProfileNames: [], bookmarks: [], s3UriPrefixObj: undefined, bookmarkStatus: { @@ -75,11 +74,10 @@ const view = createSelector( }); return { - selectedProfileName: profileName, - availableS3Profiles: s3Profiles.map(s3Profile => ({ - profileName: s3Profile.profileName, - displayName: s3Profile.paramsOfCreateS3Client.url - })), + selectedS3ProfileName: profileName, + isSelectedS3ProfileEditable: + s3Profile.origin === "created by user (or group project member)", + availableS3ProfileNames: s3Profiles.map(s3Profile => s3Profile.profileName), bookmarks: s3Profile.bookmarks, s3UriPrefixObj, bookmarkStatus: (() => { diff --git a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts index 625a10097..887d2c2eb 100644 --- a/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts +++ b/web/src/core/usecases/_s3Next/s3ExplorerRootUiController/thunks.ts @@ -55,8 +55,6 @@ export const thunks = { s3ProfilesManagement.protectedThunks.getS3ProfileAndClientForExplorer() )) ?? {}; - console.log(s3Profile); - const routeParams_toSet: RouteParams = { profile: s3Profile === undefined ? undefined : s3Profile.profileName, path: "" @@ -124,15 +122,15 @@ export const thunks = { const [dispatch, getState] = args; - const { selectedProfileName, s3UriPrefixObj, bookmarkStatus } = + const { selectedS3ProfileName, s3UriPrefixObj, bookmarkStatus } = selectors.view(getState()); - assert(selectedProfileName !== undefined); + assert(selectedS3ProfileName !== undefined); assert(s3UriPrefixObj !== undefined); await dispatch( s3ProfilesManagement.protectedThunks.createDeleteOrUpdateBookmark({ - profileName: selectedProfileName, + profileName: selectedS3ProfileName, s3UriPrefixObj, action: bookmarkStatus.isBookmarked ? { diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index 528441827..9faa51706 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -167,7 +167,7 @@ const privateThunks = { ); const { s3Client, s3Profile } = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r; @@ -275,7 +275,7 @@ const privateThunks = { const { s3Object } = params; const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -317,7 +317,7 @@ const privateThunks = { const { s3Objects } = params; const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -518,7 +518,7 @@ const privateThunks = { ); const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -550,7 +550,7 @@ export const protectedThunks = { const [dispatch] = args; const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -670,7 +670,7 @@ export const thunks = { }) ); const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -903,7 +903,7 @@ export const thunks = { ); const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -1004,7 +1004,7 @@ export const thunks = { ); const s3Client = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r.s3Client; @@ -1040,7 +1040,7 @@ export const thunks = { assert(directoryPath !== undefined); const { s3Client, s3Profile } = await dispatch( - s3ProfileManagement.protectedThunks.getS3ConfigAndClientForExplorer() + s3ProfileManagement.protectedThunks.getS3ProfileAndClientForExplorer() ).then(r => { assert(r !== undefined); return r; diff --git a/web/src/core/usecases/launcher/selectors.ts b/web/src/core/usecases/launcher/selectors.ts index 2f9400a50..67d724155 100644 --- a/web/src/core/usecases/launcher/selectors.ts +++ b/web/src/core/usecases/launcher/selectors.ts @@ -186,17 +186,8 @@ const s3ConfigSelect = createSelector( } return { - options: availableConfigs.map(s3Config => ({ - optionValue: s3Config.id, - label: { - dataSource: new URL(s3Config.paramsOfCreateS3Client.url).hostname, - friendlyName: - s3Config.origin === "created by user (or group project member)" - ? s3Config.friendlyName - : undefined - } - })), - selectedOptionValue: s3Config.s3ConfigId + options: availableConfigs.map(s3Config => s3Config.profileName), + selectedOptionValue: s3Config.s3ProfileName }; } ); @@ -220,7 +211,7 @@ const restorableConfig = createSelector( if (state === null) { return null; } - return state.s3Config.isChartUsingS3 ? state.s3Config.s3ConfigId : undefined; + return state.s3Config.isChartUsingS3 ? state.s3Config.s3ProfileName : undefined; }), helmValues, createSelector(readyState, state => { @@ -237,7 +228,7 @@ const restorableConfig = createSelector( catalogId, chartName, chartVersion, - s3ConfigId, + s3ProfileName, helmValues, helmValues_default ): projectManagement.ProjectConfigs.RestorableServiceConfig | null => { @@ -250,7 +241,7 @@ const restorableConfig = createSelector( assert(isShared !== null); assert(chartName !== null); assert(chartVersion !== null); - assert(s3ConfigId !== null); + assert(s3ProfileName !== null); assert(helmValues !== null); assert(helmValues_default !== null); @@ -265,7 +256,7 @@ const restorableConfig = createSelector( friendlyName, isShared, chartVersion, - s3ConfigId, + s3ConfigId: s3ProfileName, helmValuesPatch: diffPatch }; } @@ -318,7 +309,7 @@ const isDefaultConfiguration = createSelector( return null; } const { s3Config } = state; - return s3Config.isChartUsingS3 ? s3Config.s3ConfigId_default : undefined; + return s3Config.isChartUsingS3 ? s3Config.s3ProfileName_default : undefined; }), restorableConfig, ( @@ -326,7 +317,7 @@ const isDefaultConfiguration = createSelector( friendlyName_default, chartVersion_default, isShared_default, - s3ConfigId_default, + s3ProfileName_default, restorableConfig ) => { if (!isReady) { @@ -335,14 +326,15 @@ const isDefaultConfiguration = createSelector( assert(friendlyName_default !== null); assert(chartVersion_default !== null); assert(isShared_default !== null); - assert(s3ConfigId_default !== null); + assert(s3ProfileName_default !== null); assert(restorableConfig !== null); return ( restorableConfig.chartVersion === chartVersion_default && restorableConfig.isShared === isShared_default && restorableConfig.friendlyName === friendlyName_default && - restorableConfig.helmValuesPatch.length === 0 + restorableConfig.helmValuesPatch.length === 0 && + restorableConfig.s3ConfigId === s3ProfileName_default ); } ); diff --git a/web/src/core/usecases/launcher/state.ts b/web/src/core/usecases/launcher/state.ts index e9f3a5e4c..955150d5c 100644 --- a/web/src/core/usecases/launcher/state.ts +++ b/web/src/core/usecases/launcher/state.ts @@ -47,8 +47,8 @@ export declare namespace State { | { isChartUsingS3: false } | { isChartUsingS3: true; - s3ConfigId: string | undefined; - s3ConfigId_default: string | undefined; + s3ProfileName: string | undefined; + s3ProfileName_default: string | undefined; }; helmDependencies: { diff --git a/web/src/core/usecases/launcher/thunks.ts b/web/src/core/usecases/launcher/thunks.ts index c8ebf7961..9e2c650a8 100644 --- a/web/src/core/usecases/launcher/thunks.ts +++ b/web/src/core/usecases/launcher/thunks.ts @@ -3,7 +3,7 @@ import { assert, type Equals, is } from "tsafe/assert"; import * as userAuthentication from "../userAuthentication"; import * as deploymentRegionManagement from "core/usecases/deploymentRegionManagement"; import * as projectManagement from "core/usecases/projectManagement"; -import * as s3ConfigManagement from "core/usecases/_s3Next/s3ProfilesManagement"; +import * as s3ProfilesManagement from "core/usecases/_s3Next/s3ProfilesManagement"; import * as userConfigsUsecase from "core/usecases/userConfigs"; import * as userProfileForm from "core/usecases/userProfileForm"; import { parseUrl } from "core/tools/parseUrl"; @@ -121,7 +121,7 @@ export const thunks = { chartVersion: chartVersion_pinned, friendlyName, isShared, - s3ConfigId: s3ConfigId_pinned, + s3ConfigId: s3ProfileName_pinned, helmValuesPatch }, autoLaunch @@ -171,8 +171,8 @@ export const thunks = { const doInjectPersonalInfos = projectManagement.selectors.canInjectPersonalInfos(getState()); - const { s3ConfigId, s3ConfigId_default } = (() => { - const s3Configs = s3ConfigManagement.selectors + const { s3ProfileName, s3ProfileName_default } = (() => { + const s3Profiles = s3ProfilesManagement.selectors .s3Profiles(getState()) .filter(s3Config => doInjectPersonalInfos @@ -181,40 +181,41 @@ export const thunks = { "created by user (or group project member)" ); - const s3ConfigId_default = (() => { - const s3Config = s3Configs.find( + const s3ProfileName_default = (() => { + const s3Config = s3Profiles.find( s3Config => s3Config.isXOnyxiaDefault ); if (s3Config === undefined) { return undefined; } - return s3Config.id; + return s3Config.profileName; })(); - const s3ConfigId = (() => { - use_pinned_s3_config: { - if (s3ConfigId_pinned === undefined) { - break use_pinned_s3_config; + const s3ProfileName = (() => { + use_pinned_s3_profile: { + if (s3ProfileName_pinned === undefined) { + break use_pinned_s3_profile; } - const s3Config = s3Configs.find( - s3Config => s3Config.id === s3ConfigId_pinned + const s3Config = s3Profiles.find( + s3Profile => + s3Profile.profileName === s3ProfileName_pinned ); if (s3Config === undefined) { - break use_pinned_s3_config; + break use_pinned_s3_profile; } - return s3Config.id; + return s3Config.profileName; } - return s3ConfigId_default; + return s3ProfileName_default; })(); - return { s3ConfigId, s3ConfigId_default }; + return { s3ProfileName, s3ProfileName_default }; })(); const xOnyxiaContext = await dispatch( protectedThunks.getXOnyxiaContext({ - s3ConfigId, + s3ProfileName, doInjectPersonalInfos }) ); @@ -287,8 +288,8 @@ export const thunks = { ? { isChartUsingS3: false } : { isChartUsingS3: true, - s3ConfigId, - s3ConfigId_default + s3ProfileName, + s3ProfileName_default }, helmDependencies, @@ -576,9 +577,9 @@ const { getContext, setContext, getIsContextSet } = createUsecaseContextApi<{ export const protectedThunks = { getXOnyxiaContext: - (params: { s3ConfigId: string | undefined; doInjectPersonalInfos: boolean }) => + (params: { s3ProfileName: string | undefined; doInjectPersonalInfos: boolean }) => async (...args): Promise => { - const { s3ConfigId, doInjectPersonalInfos } = params; + const { s3ProfileName, doInjectPersonalInfos } = params; const [ dispatch, @@ -677,16 +678,16 @@ export const protectedThunks = { }; })(), s3: await (async () => { - const s3Config = (() => { - if (s3ConfigId === undefined) { + const s3Profile = (() => { + if (s3ProfileName === undefined) { return undefined; } - const s3Configs = - s3ConfigManagement.selectors.s3Profiles(getState()); + const s3Profiles = + s3ProfilesManagement.selectors.s3Profiles(getState()); - const s3Config = s3Configs.find( - s3Config => s3Config.id === s3ConfigId + const s3Config = s3Profiles.find( + s3Profile => s3Profile.profileName === s3ProfileName ); assert(s3Config !== undefined); @@ -694,13 +695,13 @@ export const protectedThunks = { return s3Config; })(); - if (s3Config === undefined) { + if (s3Profile === undefined) { return undefined; } const { host = "", port = 443 } = - s3Config.paramsOfCreateS3Client.url !== "" - ? parseUrl(s3Config.paramsOfCreateS3Client.url) + s3Profile.paramsOfCreateS3Client.url !== "" + ? parseUrl(s3Profile.paramsOfCreateS3Client.url) : {}; const s3: XOnyxiaContext["s3"] = { @@ -709,20 +710,18 @@ export const protectedThunks = { AWS_SECRET_ACCESS_KEY: undefined, AWS_SESSION_TOKEN: undefined, AWS_DEFAULT_REGION: - s3Config.paramsOfCreateS3Client.region ?? "us-east-1", + s3Profile.paramsOfCreateS3Client.region ?? "us-east-1", AWS_S3_ENDPOINT: host, port, - pathStyleAccess: s3Config.paramsOfCreateS3Client.pathStyleAccess, + pathStyleAccess: s3Profile.paramsOfCreateS3Client.pathStyleAccess, isAnonymous: false }; - if (s3Config.paramsOfCreateS3Client.isStsEnabled) { + if (s3Profile.paramsOfCreateS3Client.isStsEnabled) { const s3Client = await dispatch( - s3ConfigManagement.protectedThunks.getS3ClientForSpecificConfig( - { - s3ProfileId: s3Config.id - } - ) + s3ProfilesManagement.protectedThunks.getS3Client({ + profileName: s3Profile.profileName + }) ); const tokens = await s3Client.getToken({ doForceRenew: false }); @@ -733,14 +732,14 @@ export const protectedThunks = { s3.AWS_SECRET_ACCESS_KEY = tokens.secretAccessKey; s3.AWS_SESSION_TOKEN = tokens.sessionToken; } else if ( - s3Config.paramsOfCreateS3Client.credentials !== undefined + s3Profile.paramsOfCreateS3Client.credentials !== undefined ) { s3.AWS_ACCESS_KEY_ID = - s3Config.paramsOfCreateS3Client.credentials.accessKeyId; + s3Profile.paramsOfCreateS3Client.credentials.accessKeyId; s3.AWS_SECRET_ACCESS_KEY = - s3Config.paramsOfCreateS3Client.credentials.secretAccessKey; + s3Profile.paramsOfCreateS3Client.credentials.secretAccessKey; s3.AWS_SESSION_TOKEN = - s3Config.paramsOfCreateS3Client.credentials.sessionToken; + s3Profile.paramsOfCreateS3Client.credentials.sessionToken; } return s3; diff --git a/web/src/core/usecases/userProfileForm/thunks.ts b/web/src/core/usecases/userProfileForm/thunks.ts index 357f79790..f9ead1aa4 100644 --- a/web/src/core/usecases/userProfileForm/thunks.ts +++ b/web/src/core/usecases/userProfileForm/thunks.ts @@ -123,7 +123,7 @@ export const protectedThunks = { xOnyxiaContext: await dispatch( launcher.protectedThunks.getXOnyxiaContext({ doInjectPersonalInfos: true, - s3ConfigId: undefined + s3ProfileName: undefined }) ), helmValuesYaml: "{}", diff --git a/web/src/ui/i18n/resources/de.tsx b/web/src/ui/i18n/resources/de.tsx index a12b37c7e..e88c12a21 100644 --- a/web/src/ui/i18n/resources/de.tsx +++ b/web/src/ui/i18n/resources/de.tsx @@ -495,6 +495,61 @@ export const translations: Translations<"de"> = { SecretsExplorerItems: { "empty directory": "Dieses Verzeichnis ist leer" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Neue benutzerdefinierte S3-Konfiguration", + "dialog subtitle": + "Geben Sie ein benutzerdefiniertes Servicekonto an oder verbinden Sie einen anderen S3-kompatiblen Dienst", + cancel: "Abbrechen", + "save config": "Konfiguration speichern", + "update config": "Konfiguration aktualisieren", + "is required": "Dieses Feld ist erforderlich", + "must be an url": "Keine gültige URL", + "profile name already used": + "Ein anderes Profil mit demselben Namen existiert bereits", + "not a valid access key id": + "Dies sieht nicht nach einer gültigen Access-Key-ID aus", + "url textField label": "URL", + "url textField helper text": "URL des S3-Dienstes", + "region textField label": "AWS S3-Region", + "region textField helper text": + "Beispiel: eu-west-1, bei Unsicherheit leer lassen", + "account credentials": "Kontodaten", + "profileName textField label": "Profilname", + "profileName textField helper text": "Eindeutige Kennung dieses S3-Profils", + "isAnonymous switch label": "Anonymer Zugriff", + "isAnonymous switch helper text": + "Auf EIN stellen, wenn kein geheimer Zugriffsschlüssel erforderlich ist", + "accessKeyId textField label": "Access-Key-ID", + "accessKeyId textField helper text": "Beispiel: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Geheimer Zugriffsschlüssel", + "sessionToken textField label": "Sitzungstoken", + "sessionToken textField helper text": "Optional, bei Unsicherheit leer lassen", + "url style": "URL-Stil", + "url style helper text": + "Geben Sie an, wie Ihr S3-Server die URL zum Herunterladen von Dateien formatiert.", + "path style label": ({ example }) => ( + <> + Pfad-Stil + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Virtual-Hosted-Stil + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Nicht mehr anzeigen", "add an entry": "Einen Variable hinzufügen", diff --git a/web/src/ui/i18n/resources/en.tsx b/web/src/ui/i18n/resources/en.tsx index 35d951105..48ec41772 100644 --- a/web/src/ui/i18n/resources/en.tsx +++ b/web/src/ui/i18n/resources/en.tsx @@ -482,6 +482,56 @@ export const translations: Translations<"en"> = { "can't be empty": "Can't be empty", "new directory": "New directory" }, + CreateOrUpdateProfileDialog: { + "dialog title": "New custom S3 configuration", + "dialog subtitle": + "Specify a custom service account or connect to another S3 compatible service", + cancel: "Cancel", + "save config": "Save configuration", + "update config": "Update configuration", + "is required": "This field is required", + "must be an url": "Not a valid URL", + "profile name already used": "Another profile with same name already exists", + "not a valid access key id": "This doesn't look like a valid access key id", + "url textField label": "URL", + "url textField helper text": "URL of the S3 service", + "region textField label": "AWS S3 Region", + "region textField helper text": "Example: eu-west-1, if not sure, leave empty", + "account credentials": "Account credentials", + "profileName textField label": "Profile Name", + "profileName textField helper text": "Unique identifier of this s3 profile", + "isAnonymous switch label": "Anonymous access", + "isAnonymous switch helper text": "Set to ON if no secret access key is required", + "accessKeyId textField label": "Access key ID", + "accessKeyId textField helper text": "Example: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Secret access key", + "sessionToken textField label": "Session token", + "sessionToken textField helper text": "Optional, leave empty if not sure", + "url style": "URL style", + "url style helper text": `Specify how your S3 server formats the URL for downloading files.`, + "path style label": ({ example }) => ( + <> + Path style + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Virtual-hosted style + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Don't display again", "add an entry": "Add a new variable", diff --git a/web/src/ui/i18n/resources/es.tsx b/web/src/ui/i18n/resources/es.tsx index c6320ad27..f97d75b86 100644 --- a/web/src/ui/i18n/resources/es.tsx +++ b/web/src/ui/i18n/resources/es.tsx @@ -496,6 +496,59 @@ export const translations: Translations<"en"> = { "header size": "Tamaño", "header policy": "Política" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Nueva configuración S3 personalizada", + "dialog subtitle": + "Especifique una cuenta de servicio personalizada o conéctese a otro servicio compatible con S3", + cancel: "Cancelar", + "save config": "Guardar configuración", + "update config": "Actualizar configuración", + "is required": "Este campo es obligatorio", + "must be an url": "No es una URL válida", + "profile name already used": "Ya existe otro perfil con el mismo nombre", + "not a valid access key id": "No parece un ID de clave de acceso válido", + "url textField label": "URL", + "url textField helper text": "URL del servicio S3", + "region textField label": "Región de AWS S3", + "region textField helper text": + "Ejemplo: eu-west-1, si no está seguro, déjelo vacío", + "account credentials": "Credenciales de la cuenta", + "profileName textField label": "Nombre del perfil", + "profileName textField helper text": "Identificador único de este perfil S3", + "isAnonymous switch label": "Acceso anónimo", + "isAnonymous switch helper text": + "Poner en ON si no se requiere una clave de acceso secreta", + "accessKeyId textField label": "ID de clave de acceso", + "accessKeyId textField helper text": "Ejemplo: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Clave de acceso secreta", + "sessionToken textField label": "Token de sesión", + "sessionToken textField helper text": "Opcional, deje vacío si no está seguro", + "url style": "Estilo de URL", + "url style helper text": + "Indique cómo su servidor S3 formatea la URL para descargar archivos.", + "path style label": ({ example }) => ( + <> + Estilo de ruta + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Estilo de host virtual + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "No mostrar de nuevo", "add an entry": "Agregar una nueva variable", diff --git a/web/src/ui/i18n/resources/fi.tsx b/web/src/ui/i18n/resources/fi.tsx index 21dd86e50..55a477f95 100644 --- a/web/src/ui/i18n/resources/fi.tsx +++ b/web/src/ui/i18n/resources/fi.tsx @@ -482,6 +482,60 @@ export const translations: Translations<"fi"> = { "can't be empty": "Ei voi olla tyhjä", "new directory": "Uusi hakemisto" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Uusi mukautettu S3-määritys", + "dialog subtitle": + "Määritä mukautettu palvelutili tai yhdistä toiseen S3-yhteensopivaan palveluun", + cancel: "Peruuta", + "save config": "Tallenna määritys", + "update config": "Päivitä määritys", + "is required": "Tämä kenttä on pakollinen", + "must be an url": "Ei kelvollinen URL-osoite", + "profile name already used": "Samanniminen profiili on jo olemassa", + "not a valid access key id": + "Tämä ei näytä kelvolliselta access key -tunnukselta", + "url textField label": "URL", + "url textField helper text": "S3-palvelun URL-osoite", + "region textField label": "AWS S3 -alue", + "region textField helper text": + "Esimerkki: eu-west-1, jos et ole varma, jätä tyhjäksi", + "account credentials": "Tilin tunnistetiedot", + "profileName textField label": "Profiilin nimi", + "profileName textField helper text": "Tämän S3-profiilin yksilöllinen tunniste", + "isAnonymous switch label": "Anonyymi pääsy", + "isAnonymous switch helper text": + "Aseta ON, jos salainen access key ei ole tarpeen", + "accessKeyId textField label": "Access key -tunnus", + "accessKeyId textField helper text": "Esimerkki: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Salainen access key", + "sessionToken textField label": "Istuntotunnus", + "sessionToken textField helper text": + "Valinnainen, jätä tyhjäksi jos et ole varma", + "url style": "URL-tyyli", + "url style helper text": "Määritä, miten S3-palvelimesi muotoilee lataus-URL:n.", + "path style label": ({ example }) => ( + <> + Polkutyylinen + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Virtual-hosted-tyyli + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Älä näytä uudelleen", "add an entry": "Lisää uusi muuttuja", diff --git a/web/src/ui/i18n/resources/fr.tsx b/web/src/ui/i18n/resources/fr.tsx index 9c702654b..e6cf381e4 100644 --- a/web/src/ui/i18n/resources/fr.tsx +++ b/web/src/ui/i18n/resources/fr.tsx @@ -498,6 +498,61 @@ export const translations: Translations<"fr"> = { SecretsExplorerItems: { "empty directory": "Ce répertoire est vide" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Nouvelle configuration S3 personnalisée", + "dialog subtitle": + "Spécifiez un compte de service personnalisé ou connectez-vous à un autre service compatible S3", + cancel: "Annuler", + "save config": "Enregistrer la configuration", + "update config": "Mettre à jour la configuration", + "is required": "Ce champ est requis", + "must be an url": "URL non valide", + "profile name already used": "Un autre profil portant le même nom existe déjà", + "not a valid access key id": + "Cela ne ressemble pas à un ID de clé d'accès valide", + "url textField label": "URL", + "url textField helper text": "URL du service S3", + "region textField label": "Région AWS S3", + "region textField helper text": + "Exemple : eu-west-1, si vous n'êtes pas sûr, laissez vide", + "account credentials": "Identifiants du compte", + "profileName textField label": "Nom du profil", + "profileName textField helper text": "Identifiant unique de ce profil S3", + "isAnonymous switch label": "Accès anonyme", + "isAnonymous switch helper text": + "Mettre sur ON si aucune clé d'accès secrète n'est requise", + "accessKeyId textField label": "ID de clé d'accès", + "accessKeyId textField helper text": "Exemple : 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Clé d'accès secrète", + "sessionToken textField label": "Jeton de session", + "sessionToken textField helper text": + "Optionnel, laissez vide si vous n'êtes pas sûr", + "url style": "Style d'URL", + "url style helper text": + "Indiquez comment votre serveur S3 formate l'URL pour télécharger des fichiers.", + "path style label": ({ example }) => ( + <> + Style chemin + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Style virtual-hosted + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Ne plus afficher", "add an entry": "Ajouter une variable", diff --git a/web/src/ui/i18n/resources/it.tsx b/web/src/ui/i18n/resources/it.tsx index 239268146..2b48524a5 100644 --- a/web/src/ui/i18n/resources/it.tsx +++ b/web/src/ui/i18n/resources/it.tsx @@ -402,9 +402,8 @@ export const translations: Translations<"it"> = { la nostra documentazione .   - - Configurare il tuo Vault CLI locale - . + Configurare il tuo Vault CLI locale + . ) }, @@ -493,6 +492,60 @@ export const translations: Translations<"it"> = { SecretsExplorerItems: { "empty directory": "Questa cartella è vuota" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Nuova configurazione S3 personalizzata", + "dialog subtitle": + "Specifica un account di servizio personalizzato o connettiti a un altro servizio compatibile con S3", + cancel: "Annulla", + "save config": "Salva configurazione", + "update config": "Aggiorna configurazione", + "is required": "Questo campo è obbligatorio", + "must be an url": "URL non valido", + "profile name already used": "Esiste già un altro profilo con lo stesso nome", + "not a valid access key id": "Non sembra un ID di access key valido", + "url textField label": "URL", + "url textField helper text": "URL del servizio S3", + "region textField label": "Regione AWS S3", + "region textField helper text": + "Esempio: eu-west-1, se non sei sicuro lascia vuoto", + "account credentials": "Credenziali dell'account", + "profileName textField label": "Nome profilo", + "profileName textField helper text": + "Identificatore univoco di questo profilo S3", + "isAnonymous switch label": "Accesso anonimo", + "isAnonymous switch helper text": + "Imposta su ON se non è necessaria una secret access key", + "accessKeyId textField label": "ID access key", + "accessKeyId textField helper text": "Esempio: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Secret access key", + "sessionToken textField label": "Token di sessione", + "sessionToken textField helper text": "Opzionale, lascia vuoto se non sei sicuro", + "url style": "Stile URL", + "url style helper text": + "Specifica come il server S3 formatta l'URL per scaricare i file.", + "path style label": ({ example }) => ( + <> + Stile path + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Stile virtual-hosted + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Non mostrare più", "add an entry": "Aggiungiere una variabile", diff --git a/web/src/ui/i18n/resources/nl.tsx b/web/src/ui/i18n/resources/nl.tsx index e5bc0f0e2..663cd2e35 100644 --- a/web/src/ui/i18n/resources/nl.tsx +++ b/web/src/ui/i18n/resources/nl.tsx @@ -493,6 +493,58 @@ export const translations: Translations<"nl"> = { SecretsExplorerItems: { "empty directory": "Deze bestandenlijst is leeg" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Nieuwe aangepaste S3-configuratie", + "dialog subtitle": + "Specificeer een aangepast serviceaccount of verbind met een andere S3-compatibele dienst", + cancel: "Annuleren", + "save config": "Configuratie opslaan", + "update config": "Configuratie bijwerken", + "is required": "Dit veld is verplicht", + "must be an url": "Geen geldige URL", + "profile name already used": "Er bestaat al een ander profiel met dezelfde naam", + "not a valid access key id": "Dit lijkt geen geldige access key-id", + "url textField label": "URL", + "url textField helper text": "URL van de S3-dienst", + "region textField label": "AWS S3-regio", + "region textField helper text": "Voorbeeld: eu-west-1, bij twijfel leeg laten", + "account credentials": "Accountgegevens", + "profileName textField label": "Profielnaam", + "profileName textField helper text": "Unieke identificatie van dit S3-profiel", + "isAnonymous switch label": "Anonieme toegang", + "isAnonymous switch helper text": + "Zet op ON als geen geheime access key nodig is", + "accessKeyId textField label": "Access key-id", + "accessKeyId textField helper text": "Voorbeeld: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Geheime access key", + "sessionToken textField label": "Sessietoken", + "sessionToken textField helper text": "Optioneel, leeg laten bij twijfel", + "url style": "URL-stijl", + "url style helper text": + "Geef aan hoe je S3-server de URL voor het downloaden van bestanden opmaakt.", + "path style label": ({ example }) => ( + <> + Padstijl + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Virtual-hosted-stijl + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Niet meer weergeven", "add an entry": "Een variabele toevoegen", diff --git a/web/src/ui/i18n/resources/no.tsx b/web/src/ui/i18n/resources/no.tsx index 8d6ff4a22..437babdc8 100644 --- a/web/src/ui/i18n/resources/no.tsx +++ b/web/src/ui/i18n/resources/no.tsx @@ -491,6 +491,59 @@ export const translations: Translations<"no"> = { "header size": "Størrelse", "header policy": "Retningslinje" }, + CreateOrUpdateProfileDialog: { + "dialog title": "Ny tilpasset S3-konfigurasjon", + "dialog subtitle": + "Angi en tilpasset tjenestekonto eller koble til en annen S3-kompatibel tjeneste", + cancel: "Avbryt", + "save config": "Lagre konfigurasjon", + "update config": "Oppdater konfigurasjon", + "is required": "Dette feltet er påkrevd", + "must be an url": "Ugyldig URL", + "profile name already used": "En annen profil med samme navn finnes allerede", + "not a valid access key id": "Dette ser ikke ut som en gyldig access key-ID", + "url textField label": "URL", + "url textField helper text": "URL til S3-tjenesten", + "region textField label": "AWS S3-region", + "region textField helper text": + "Eksempel: eu-west-1, hvis du er usikker, la stå tomt", + "account credentials": "Kontoopplysninger", + "profileName textField label": "Profilnavn", + "profileName textField helper text": "Unik identifikator for denne S3-profilen", + "isAnonymous switch label": "Anonym tilgang", + "isAnonymous switch helper text": + "Sett til PÅ hvis ingen hemmelig access key er nødvendig", + "accessKeyId textField label": "Access key-ID", + "accessKeyId textField helper text": "Eksempel: 1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Hemmelig access key", + "sessionToken textField label": "Økttoken", + "sessionToken textField helper text": "Valgfritt, la stå tomt hvis du er usikker", + "url style": "URL-stil", + "url style helper text": + "Angi hvordan S3-serveren din formaterer URL-en for nedlasting av filer.", + "path style label": ({ example }) => ( + <> + Sti-stil + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + Virtual-hosted-stil + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "Ikke vis igjen", "add an entry": "Legg til en ny variabel", diff --git a/web/src/ui/i18n/resources/zh-CN.tsx b/web/src/ui/i18n/resources/zh-CN.tsx index 35b02e127..cb8690d1b 100644 --- a/web/src/ui/i18n/resources/zh-CN.tsx +++ b/web/src/ui/i18n/resources/zh-CN.tsx @@ -453,6 +453,55 @@ export const translations: Translations<"zh-CN"> = { SecretsExplorerItems: { "empty directory": "此目录为空" }, + CreateOrUpdateProfileDialog: { + "dialog title": "新的自定义 S3 配置", + "dialog subtitle": "指定自定义服务账号或连接到其他兼容 S3 的服务", + cancel: "取消", + "save config": "保存配置", + "update config": "更新配置", + "is required": "此字段为必填项", + "must be an url": "不是有效的 URL", + "profile name already used": "已存在同名的配置文件", + "not a valid access key id": "看起来不是有效的 Access Key ID", + "url textField label": "URL", + "url textField helper text": "S3 服务的 URL", + "region textField label": "AWS S3 区域", + "region textField helper text": "示例:eu-west-1,不确定可留空", + "account credentials": "账号凭证", + "profileName textField label": "配置文件名称", + "profileName textField helper text": "此 S3 配置文件的唯一标识", + "isAnonymous switch label": "匿名访问", + "isAnonymous switch helper text": "若不需要 Secret Access Key,请设为开", + "accessKeyId textField label": "Access Key ID", + "accessKeyId textField helper text": "示例:1A2B3C4D5E6F7G8H9I0J", + "secretAccessKey textField label": "Secret Access Key", + "sessionToken textField label": "会话令牌", + "sessionToken textField helper text": "可选,不确定可留空", + "url style": "URL 样式", + "url style helper text": "指定你的 S3 服务器用于下载文件的 URL 格式。", + "path style label": ({ example }) => ( + <> + 路径样式 + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ), + "virtual-hosted style label": ({ example }) => ( + <> + 虚拟主机样式 + {example !== undefined && ( + <> + :  + {example}my-dataset.parquet + + )} + + ) + }, MySecretsEditor: { "do not display again": "不要再显示", "add an entry": "添加变量", diff --git a/web/src/ui/i18n/types.ts b/web/src/ui/i18n/types.ts index cd38cdb01..df0d6c1bc 100644 --- a/web/src/ui/i18n/types.ts +++ b/web/src/ui/i18n/types.ts @@ -21,6 +21,7 @@ export type ComponentKey = | import("ui/pages/fileExplorerEntry/FileExplorerDisabledDialog").I18n | import("ui/pages/s3Explorer/ConfirmBucketCreationAttemptDialog").I18n | import("ui/pages/s3Explorer/Explorer").I18n + | import("ui/pages/s3Explorer/S3ConfigDialogs/CreateOrUpdateProfileDialog").I18n | import("ui/pages/fileExplorer/Explorer/Explorer").I18n | import("ui/pages/fileExplorer/Explorer/ExplorerButtonBar").I18n | import("ui/pages/fileExplorer/Explorer/ExplorerItems").I18n diff --git a/web/src/ui/pages/launcher/LauncherMainCard.tsx b/web/src/ui/pages/launcher/LauncherMainCard.tsx index a3ce7e8ac..253280280 100644 --- a/web/src/ui/pages/launcher/LauncherMainCard.tsx +++ b/web/src/ui/pages/launcher/LauncherMainCard.tsx @@ -77,13 +77,7 @@ export type Props = { | { projectS3ConfigLink: Link; selectedOption: string | undefined; - options: { - optionValue: string; - label: { - dataSource: string; - friendlyName: string | undefined; - }; - }[]; + options: string[]; onSelectedS3ConfigChange: (params: { s3ConfigId: string }) => void; } | undefined; @@ -317,12 +311,9 @@ export const LauncherMainCard = memo((props: Props) => { {" "} - {s3ConfigsSelect.options.map(({ label, optionValue }) => ( - - {label.friendlyName !== undefined - ? `${label.friendlyName} - ` - : ""} - {label.dataSource} + {s3ConfigsSelect.options.map(profileName => ( + + {profileName} ))} diff --git a/web/src/ui/pages/s3Explorer/Page.tsx b/web/src/ui/pages/s3Explorer/Page.tsx index 9586932dd..5373bbac2 100644 --- a/web/src/ui/pages/s3Explorer/Page.tsx +++ b/web/src/ui/pages/s3Explorer/Page.tsx @@ -58,7 +58,7 @@ function S3Explorer() { evts: { evtS3ExplorerRootUiController } } = getCoreSync(); - const { selectedS3ProfileId, s3UriPrefixObj, bookmarkStatus } = useCoreState( + const { selectedS3ProfileName, s3UriPrefixObj, bookmarkStatus } = useCoreState( "s3ExplorerRootUiController", "view" ); @@ -105,14 +105,15 @@ function S3Explorer() { className={css({ marginTop: theme.spacing(5), display: - selectedS3ProfileId === undefined || s3UriPrefixObj !== undefined + selectedS3ProfileName === undefined || + s3UriPrefixObj !== undefined ? "none" : undefined })} /> {(() => { - if (selectedS3ProfileId === undefined) { + if (selectedS3ProfileName === undefined) { return

Create a profile

; } @@ -335,14 +336,17 @@ function S3ProfileSelect() { functions: { s3ExplorerRootUiController } } = getCoreSync(); - const { selectedS3ProfileId, selectedS3Profile_creationTime, availableS3Profiles } = - useCoreState("s3ExplorerRootUiController", "view"); + const { + selectedS3ProfileName, + availableS3ProfileNames, + isSelectedS3ProfileEditable + } = useCoreState("s3ExplorerRootUiController", "view"); const { css } = useStyles(); const { evtConfirmCustomS3ConfigDeletionDialogOpen, - evtAddCustomS3ConfigDialogOpen, + evtCreateOrUpdateProfileDialogOpen, evtMaybeAcknowledgeConfigVolatilityDialogOpen } = useConst(() => ({ evtConfirmCustomS3ConfigDeletionDialogOpen: @@ -351,9 +355,9 @@ function S3ProfileSelect() { S3ConfigDialogsProps["evtConfirmCustomS3ConfigDeletionDialogOpen"] > >(), - evtAddCustomS3ConfigDialogOpen: + evtCreateOrUpdateProfileDialogOpen: Evt.create< - UnpackEvt + UnpackEvt >(), evtMaybeAcknowledgeConfigVolatilityDialogOpen: Evt.create() @@ -365,7 +369,7 @@ function S3ProfileSelect() { S3 Profile - {selectedS3Profile_creationTime !== undefined && ( - - )} + {isSelectedS3ProfileEditable && + (() => { + assert(selectedS3ProfileName !== undefined); + + return ( + + ); + })()} ; }; -export const AddCustomS3ConfigDialog = memo((props: AddCustomS3ConfigDialogProps) => { - const { evtOpen } = props; +export const CreateOrUpdateProfileDialog = memo( + (props: CreateOrUpdateProfileDialogProps) => { + const { evtOpen } = props; - const { t } = useTranslation({ AddCustomS3ConfigDialog }); + const { t } = useTranslation({ CreateOrUpdateProfileDialog }); - const { - functions: { s3ProfilesCreationUiController } - } = getCoreSync(); + const { + functions: { s3ProfilesCreationUiController } + } = getCoreSync(); - const { isReady } = useCoreState("s3ProfilesCreationUiController", "main"); + const { isReady } = useCoreState("s3ProfilesCreationUiController", "main"); - useEvt( - ctx => - evtOpen.attach(ctx, ({ creationTimeOfS3ProfileToEdit }) => - s3ProfilesCreationUiController.initialize({ - creationTimeOfS3ProfileToEdit - }) - ), - [evtOpen] - ); + useEvt( + ctx => + evtOpen.attach(ctx, ({ profileName_toUpdate }) => + s3ProfilesCreationUiController.initialize({ + profileName_toUpdate + }) + ), + [evtOpen] + ); - const onCloseFactory = useCallbackFactory(([isSubmit]: [boolean]) => { - if (isSubmit) { - s3ProfilesCreationUiController.submit(); - } else { - s3ProfilesCreationUiController.reset(); - } - }); + const onCloseFactory = useCallbackFactory(([isSubmit]: [boolean]) => { + if (isSubmit) { + s3ProfilesCreationUiController.submit(); + } else { + s3ProfilesCreationUiController.reset(); + } + }); - const { classes } = useStyles(); + const { classes } = useStyles(); - return ( - } - buttons={ - - } - onClose={onCloseFactory(false)} - /> - ); -}); + return ( + } + buttons={ + + } + onClose={onCloseFactory(false)} + /> + ); + } +); -AddCustomS3ConfigDialog.displayName = symToStr({ - AddCustomS3ConfigDialog +CreateOrUpdateProfileDialog.displayName = symToStr({ + CreateOrUpdateProfileDialog }); -const useStyles = tss.withName({ AddCustomS3ConfigDialog }).create({ +const useStyles = tss.withName({ CreateOrUpdateProfileDialog }).create({ buttons: { display: "flex" } @@ -103,7 +105,7 @@ const Buttons = memo((props: ButtonsProps) => { const { css } = useButtonsStyles(); - const { t } = useTranslation({ AddCustomS3ConfigDialog }); + const { t } = useTranslation({ CreateOrUpdateProfileDialog }); if (!isReady) { return null; @@ -123,7 +125,7 @@ const Buttons = memo((props: ButtonsProps) => { }); const useButtonsStyles = tss - .withName(`${symToStr({ AddCustomS3ConfigDialog })}${symToStr({ Buttons })}`) + .withName(`${symToStr({ CreateOrUpdateProfileDialog })}${symToStr({ Buttons })}`) .create({}); const Body = memo(() => { @@ -138,7 +140,7 @@ const Body = memo(() => { const { classes, css, theme } = useBodyStyles(); - const { t } = useTranslation({ AddCustomS3ConfigDialog }); + const { t } = useTranslation({ CreateOrUpdateProfileDialog }); if (!isReady) { return null; @@ -151,18 +153,18 @@ const Body = memo(() => { className={css({ marginBottom: theme.spacing(6) })} - label={t("friendlyName textField label")} - helperText={t("friendlyName textField helper text")} + label={t("profileName textField label")} + helperText={t("profileName textField helper text")} helperTextError={ - formValuesErrors.friendlyName === undefined + formValuesErrors.profileName === undefined ? undefined - : t(formValuesErrors.friendlyName) + : t(formValuesErrors.profileName) } - defaultValue={formValues.friendlyName} + defaultValue={formValues.profileName} doOnlyShowErrorAfterFirstFocusLost onValueBeingTypedChange={({ value }) => s3ProfilesCreationUiController.changeValue({ - key: "friendlyName", + key: "profileName", value }) } @@ -352,7 +354,7 @@ const Body = memo(() => { }); const useBodyStyles = tss - .withName(`${symToStr({ AddCustomS3ConfigDialog })}${symToStr({ Body })}`) + .withName(`${symToStr({ CreateOrUpdateProfileDialog })}${symToStr({ Body })}`) .create(({ theme }) => ({ serverConfigFormGroup: { display: "flex", @@ -381,19 +383,15 @@ const { i18n } = declareComponentKeys< | "update config" | "is required" | "must be an url" + | "profile name already used" | "not a valid access key id" | "url textField label" | "url textField helper text" | "region textField label" | "region textField helper text" - | "workingDirectoryPath textField label" - | { - K: "workingDirectoryPath textField helper text"; - R: JSX.Element; - } | "account credentials" - | "friendlyName textField label" - | "friendlyName textField helper text" + | "profileName textField label" + | "profileName textField helper text" | "isAnonymous switch label" | "isAnonymous switch helper text" | "accessKeyId textField label" @@ -413,5 +411,5 @@ const { i18n } = declareComponentKeys< P: { example: string | undefined }; R: JSX.Element; } ->()({ AddCustomS3ConfigDialog }); +>()({ CreateOrUpdateProfileDialog }); export type I18n = typeof i18n; diff --git a/web/src/ui/pages/s3Explorer/S3ConfigDialogs/S3ConfigDialogs.tsx b/web/src/ui/pages/s3Explorer/S3ConfigDialogs/S3ConfigDialogs.tsx index 6580ca930..78086075c 100644 --- a/web/src/ui/pages/s3Explorer/S3ConfigDialogs/S3ConfigDialogs.tsx +++ b/web/src/ui/pages/s3Explorer/S3ConfigDialogs/S3ConfigDialogs.tsx @@ -3,25 +3,27 @@ import { type Props as ConfirmCustomS3ConfigDeletionDialogProps } from "./ConfirmCustomS3ConfigDeletionDialog"; import { - AddCustomS3ConfigDialog, - type AddCustomS3ConfigDialogProps -} from "./AddCustomS3ConfigDialog"; + CreateOrUpdateProfileDialog, + type CreateOrUpdateProfileDialogProps +} from "./CreateOrUpdateProfileDialog"; export type S3ConfigDialogsProps = { evtConfirmCustomS3ConfigDeletionDialogOpen: ConfirmCustomS3ConfigDeletionDialogProps["evtOpen"]; - evtAddCustomS3ConfigDialogOpen: AddCustomS3ConfigDialogProps["evtOpen"]; + evtCreateOrUpdateProfileDialogOpen: CreateOrUpdateProfileDialogProps["evtOpen"]; }; export function S3ConfigDialogs(props: S3ConfigDialogsProps) { - const { evtConfirmCustomS3ConfigDeletionDialogOpen, evtAddCustomS3ConfigDialogOpen } = - props; + const { + evtConfirmCustomS3ConfigDeletionDialogOpen, + evtCreateOrUpdateProfileDialogOpen + } = props; return ( <> - + ); } From 4847ff2b4b9ccc793721ffefd4ec6e8d50112d77 Mon Sep 17 00:00:00 2001 From: garronej Date: Mon, 26 Jan 2026 20:23:29 +0100 Subject: [PATCH 11/11] Temporary patch before deleting persistance of s3 profile defaults --- .../updateDefaultS3ProfilesAfterPotentialDeletion.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts index 49480fb09..5e4d04325 100644 --- a/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts +++ b/web/src/core/usecases/_s3Next/s3ProfilesManagement/decoupledLogic/updateDefaultS3ProfilesAfterPotentialDeletion.ts @@ -23,7 +23,12 @@ export function updateDefaultS3ProfilesAfterPotentialDeletion(params: { const s3Profiles = aggregateS3ProfilesFromVaultAndRegionIntoAnUnifiedSet({ fromRegion: { - s3Profiles: fromRegion.s3Profiles, + s3Profiles: fromRegion.s3Profiles.map(s3Profile => { + return { + ...s3Profile, + profileName: s3Profile.profileName ?? "" + }; + }), resolvedTemplatedBookmarks: undefined, resolvedTemplatedStsRoles: undefined },