├── .gitattributes ├── helm-chart ├── templates │ ├── NOTES.txt │ ├── web │ │ ├── serviceaccount.yaml │ │ ├── service.yaml │ │ └── route.yaml │ ├── api │ │ ├── serviceaccount-api.yaml │ │ ├── service.yaml │ │ ├── rolebinding-namespace-admin.yaml │ │ ├── configmaps-userprofile.yaml │ │ ├── cluster-role-binding.yaml │ │ ├── configmaps-schemas.yaml │ │ ├── configmap.yaml │ │ └── route.yaml │ ├── tests │ │ ├── test-connection-api.yaml │ │ └── test-connection-ihm.yaml │ └── onboarding │ │ ├── cluster-role.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── cluster-role-binding.yaml ├── .helmignore └── Chart.yaml ├── web ├── .husky │ └── pre-commit ├── src │ ├── keycloak-theme │ │ ├── email │ │ │ ├── theme.properties │ │ │ ├── text │ │ │ │ └── email-verification.ftl │ │ │ └── html │ │ │ │ └── email-verification.ftl │ │ ├── login │ │ │ ├── KcContext.ts │ │ │ ├── tools │ │ │ │ └── getReferrerUrl.ts │ │ │ └── theme.ts │ │ └── main.tsx │ ├── core │ │ ├── adapters │ │ │ ├── oidc │ │ │ │ ├── index.ts │ │ │ │ └── mock.ts │ │ │ ├── onyxiaApi │ │ │ │ └── index.ts │ │ │ ├── s3Client │ │ │ │ ├── index.ts │ │ │ │ └── utils │ │ │ │ │ └── bucketNameAndObjectNameFromS3Path.ts │ │ │ ├── sqlOlap │ │ │ │ └── index.ts │ │ │ └── secretManager │ │ │ │ └── index.ts │ │ ├── tools │ │ │ ├── ArrayOrNot.ts │ │ │ ├── DeepPartial.ts │ │ │ ├── Omit.ts │ │ │ ├── generateRandomPassword.ts │ │ │ ├── timeFormat │ │ │ │ ├── type.ts │ │ │ │ └── constants.ts │ │ │ ├── Stringifyable │ │ │ │ ├── getIsAtomic.ts │ │ │ │ ├── index.ts │ │ │ │ ├── getDoesPathStartWith.ts │ │ │ │ └── assignValueAtPath.test.ts │ │ │ ├── fnv1aHashToHex.ts │ │ │ ├── OptionalIfCanBeUndefined.ts │ │ │ ├── waitForDebounce.ts │ │ │ ├── Object.fromEntries.ts │ │ │ ├── streamToArrayBuffer.ts │ │ │ ├── highlightMatches.ts │ │ │ ├── structuredCloneButFunctions.ts │ │ │ ├── parseUrl.ts │ │ │ ├── getValueAtPathInObject.ts │ │ │ ├── timeAgo.ts │ │ │ └── bytes.ts │ │ ├── usecases │ │ │ ├── launcher │ │ │ │ ├── decoupledLogic │ │ │ │ │ ├── computeRootForm │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── mergeRangeSliders │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── mutateHelmValues │ │ │ │ │ │ ├── mutateHelmValues_update │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── mutateHelmValues_removeArrayItem.test.ts │ │ │ │ │ │ └── mutateHelmValues_removeArrayItem.ts │ │ │ │ │ ├── shared │ │ │ │ │ │ ├── validateValueAgainstJSONSchema │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── getHelmValuesPathDeeperCommonSubpath.test.ts │ │ │ │ │ │ └── getHelmValuesPathDeeperCommonSubpath.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── podLogs │ │ │ │ └── index.ts │ │ │ ├── viewQuotas │ │ │ │ └── index.ts │ │ │ ├── fileExplorer │ │ │ │ └── index.ts │ │ │ ├── projectManagement │ │ │ │ ├── decoupledLogic │ │ │ │ │ ├── projectConfigsMigration │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── projectVaultTopDirPath_reserved.ts │ │ │ │ ├── index.ts │ │ │ │ └── state.ts │ │ │ ├── s3CodeSnippets │ │ │ │ └── index.ts │ │ │ ├── userProfileForm │ │ │ │ └── index.ts │ │ │ ├── autoLogoutCountdown │ │ │ │ ├── index.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── state.ts │ │ │ ├── s3ConfigCreation │ │ │ │ └── index.ts │ │ │ ├── serviceManagement │ │ │ │ └── index.ts │ │ │ ├── userAuthentication │ │ │ │ ├── index.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── state.ts │ │ │ ├── s3ConfigConnectionTest │ │ │ │ ├── index.ts │ │ │ │ └── selectors.ts │ │ │ ├── deploymentRegionManagement │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── selectors.ts │ │ │ ├── catalog │ │ │ │ ├── index.ts │ │ │ │ └── evt.ts │ │ │ ├── dataExplorer │ │ │ │ └── index.ts │ │ │ ├── dataCollection │ │ │ │ └── index.ts │ │ │ ├── serviceDetails │ │ │ │ ├── index.ts │ │ │ │ └── evt.ts │ │ │ ├── clusterEventsMonitor │ │ │ │ └── index.ts │ │ │ ├── sqlOlapShell │ │ │ │ └── index.ts │ │ │ ├── s3ConfigManagement │ │ │ │ ├── index.ts │ │ │ │ ├── decoupledLogic │ │ │ │ │ └── projectS3ConfigId.ts │ │ │ │ └── state.ts │ │ │ └── restorableConfigManagement │ │ │ │ ├── index.ts │ │ │ │ ├── decoupledLogic │ │ │ │ └── getAreSameRestorableConfigRef.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── state.ts │ │ ├── ports │ │ │ ├── OnyxiaApi │ │ │ │ ├── User.ts │ │ │ │ ├── Project.ts │ │ │ │ ├── Chart.ts │ │ │ │ ├── Catalog.ts │ │ │ │ ├── OidcParams.ts │ │ │ │ ├── HelmRelease.ts │ │ │ │ ├── index.ts │ │ │ │ └── Language.ts │ │ │ └── SecretsManager.ts │ │ └── index.ts │ ├── ui │ │ ├── App │ │ │ ├── Header │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ └── logoContainerWidthInPercent.ts │ │ ├── pages │ │ │ ├── myServices │ │ │ │ ├── Quotas │ │ │ │ │ ├── index.ts │ │ │ │ │ └── CircularUsage.stories.tsx │ │ │ │ ├── MyServicesCards │ │ │ │ │ ├── index.ts │ │ │ │ │ └── MyServicesCard │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── ReadmeDialog │ │ │ │ │ │ └── index.ts │ │ │ │ ├── MyServicesRestorableConfigs │ │ │ │ │ ├── index.ts │ │ │ │ │ └── MyServicesRestorableConfig │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── MyServicesRestorableConfigOptions.stories.tsx │ │ │ │ ├── index.ts │ │ │ │ └── route.ts │ │ │ ├── fileExplorer │ │ │ │ ├── Explorer │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ExplorerItems │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── ExplorerUploadModal │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── ExplorerUploadModalDropArea.stories.tsx │ │ │ │ │ │ ├── ExplorerUploadModal.stories.tsx │ │ │ │ │ │ └── ExplorerUploadProgress.stories.tsx │ │ │ │ │ ├── ExplorerIcon │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── ExplorerIcon.stories.tsx │ │ │ │ │ └── ExplorerButtonBar.stories.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── shared │ │ │ │ │ ├── tools.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── DirectoryOrFileDetailed.stories.tsx │ │ │ │ ├── route.ts │ │ │ │ └── ShareFile │ │ │ │ │ └── SelectTime.stories.tsx │ │ │ ├── launcher │ │ │ │ ├── LauncherDialogs │ │ │ │ │ └── index.ts │ │ │ │ ├── RootFormComponent │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── FormFieldGroupComponent │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── AutoInjectSwitch.stories.tsx │ │ │ │ │ │ └── AutoInjectSwitch.tsx │ │ │ │ │ └── FormCallbacks.d.ts │ │ │ │ └── index.ts │ │ │ ├── mySecrets │ │ │ │ ├── MySecretsEditor │ │ │ │ │ ├── index.ts │ │ │ │ │ └── MySecretsEditorRow.stories.tsx │ │ │ │ ├── SecretsExplorer │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SecretsExplorerItems │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── ExplorerIcon.stories.tsx │ │ │ │ │ └── SecretsExplorerButtonBar.stories.tsx │ │ │ │ ├── index.ts │ │ │ │ └── route.ts │ │ │ ├── projectSettings │ │ │ │ ├── ProjectSettingsS3ConfigTab │ │ │ │ │ ├── index.ts │ │ │ │ │ └── S3ConfigDialogs │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── S3ConfigDialogs.tsx │ │ │ │ ├── tabIds.ts │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ └── ProjectSettingsSecurityInfosTab.tsx │ │ │ ├── account │ │ │ │ ├── AccountProfileTab │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── accountTabIds.ts │ │ │ │ └── route.ts │ │ │ ├── dataCollection │ │ │ │ ├── index.ts │ │ │ │ └── route.ts │ │ │ ├── home │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ └── LinkFromConfigButton.tsx │ │ │ ├── catalog │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ ├── CatalogNoSearchMatches.stories.tsx │ │ │ │ └── CatalogSwitcherButton.stories.tsx │ │ │ ├── document │ │ │ │ ├── index.ts │ │ │ │ ├── Page.tsx │ │ │ │ └── route.ts │ │ │ ├── myService │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ └── MyServiceButtonBar.stories.tsx │ │ │ ├── page404 │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ ├── Page404.stories.tsx │ │ │ │ └── Page.tsx │ │ │ ├── dataExplorer │ │ │ │ ├── index.ts │ │ │ │ ├── UrlInput.stories.tsx │ │ │ │ └── route.ts │ │ │ ├── sqlOlapShell │ │ │ │ ├── index.ts │ │ │ │ └── route.ts │ │ │ └── fileExplorerEntry │ │ │ │ ├── index.ts │ │ │ │ ├── route.ts │ │ │ │ └── FileExplorerDisabledDialog.stories.tsx │ │ ├── shared │ │ │ ├── textEditor │ │ │ │ ├── CodeTextEditor │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── JsonCodeTextEditor.tsx │ │ │ │ │ └── ShellCodeTextEditor.tsx │ │ │ │ ├── README.md │ │ │ │ ├── tss.ts │ │ │ │ └── DataTextEditor │ │ │ │ │ ├── JsonSchemaDialog.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── formattedDate │ │ │ │ ├── index.ts │ │ │ │ ├── getFormattedDate.ts │ │ │ │ ├── type.ts │ │ │ │ └── useFormattedDate.ts │ │ │ ├── enforceLogin.ts │ │ │ ├── LoadingDots.stories.tsx │ │ │ ├── MaybeLink.tsx │ │ │ ├── PortraitModeUnsupported.stories.tsx │ │ │ ├── RoundLogo.stories.tsx │ │ │ ├── CircularMetric.stories.tsx │ │ │ ├── CopyToClipboardIconButton.stories.tsx │ │ │ ├── BrandHeaderSection.stories.tsx │ │ │ ├── SettingSectionHeader.stories.tsx │ │ │ ├── SettingField.stories.tsx │ │ │ ├── Datagrid │ │ │ │ └── CustomNoRowsOverlay.tsx │ │ │ ├── CodeBlock.tsx │ │ │ ├── CopyToClipboardIconButton.tsx │ │ │ ├── CommandBar.stories.tsx │ │ │ ├── LinkFromConfig.ts │ │ │ └── ensureUrlIsSafe.ts │ │ ├── theme │ │ │ ├── targetWindowInnerWidth.ts │ │ │ ├── index.ts │ │ │ ├── emotionCache.ts │ │ │ └── palette.ts │ │ ├── assets │ │ │ ├── img │ │ │ │ ├── ProConnect_dark.webp │ │ │ │ ├── ProConnect_light.webp │ │ │ │ ├── ProConnect_dark_hover.webp │ │ │ │ └── ProConnect_light_hover.webp │ │ │ └── svg │ │ │ │ ├── explorer │ │ │ │ ├── favicon.svg │ │ │ │ ├── html.svg │ │ │ │ ├── markdown.svg │ │ │ │ ├── svg.svg │ │ │ │ ├── vue.svg │ │ │ │ ├── text.svg │ │ │ │ ├── video.svg │ │ │ │ ├── css.svg │ │ │ │ ├── image.svg │ │ │ │ ├── csv.svg │ │ │ │ ├── yml.svg │ │ │ │ ├── lua.svg │ │ │ │ ├── xml.svg │ │ │ │ ├── directory.svg │ │ │ │ ├── javascript.svg │ │ │ │ ├── typescript.svg │ │ │ │ ├── java.svg │ │ │ │ ├── scala.svg │ │ │ │ ├── illustrator.svg │ │ │ │ ├── photoshop.svg │ │ │ │ ├── tex.svg │ │ │ │ ├── json.svg │ │ │ │ ├── github.svg │ │ │ │ ├── secret.svg │ │ │ │ ├── python.svg │ │ │ │ ├── sass.svg │ │ │ │ ├── audio.svg │ │ │ │ ├── xls.svg │ │ │ │ ├── zip.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── config.svg │ │ │ │ ├── word.svg │ │ │ │ └── R.svg │ │ │ │ ├── Menu.svg │ │ │ │ ├── ArrowLeft.svg │ │ │ │ ├── ArrowBottom.svg │ │ │ │ ├── ArrowTop.svg │ │ │ │ ├── ArrowRight.svg │ │ │ │ ├── HomeIconStock.svg │ │ │ │ ├── CheckboxEmpty.svg │ │ │ │ ├── Plus.svg │ │ │ │ ├── Launch.svg │ │ │ │ ├── CheckboxActive.svg │ │ │ │ ├── Close.svg │ │ │ │ ├── Edit.svg │ │ │ │ ├── Build.svg │ │ │ │ ├── Delete.svg │ │ │ │ ├── More.svg │ │ │ │ ├── Search.svg │ │ │ │ ├── Folder.svg │ │ │ │ ├── singlePackage.svg │ │ │ │ ├── Copy.svg │ │ │ │ ├── HomeIcon.svg │ │ │ │ ├── User.svg │ │ │ │ ├── KeyFile.svg │ │ │ │ ├── Directory.svg │ │ │ │ └── ServiceNotFound.svg │ │ ├── tools │ │ │ ├── copyToClipboard.ts │ │ │ ├── isStorybook.ts │ │ │ ├── triggerBrowserDonwload.ts │ │ │ ├── areSameSet.ts │ │ │ ├── JSONSortStringify.ts │ │ │ ├── getIsJSON5ObjectOrArray.ts │ │ │ ├── getBrowser.ts │ │ │ ├── simpleHash.ts │ │ │ ├── parseCssSpacing.ts │ │ │ ├── emailRegExp.ts │ │ │ ├── getPathDepth.ts │ │ │ ├── use.ts │ │ │ ├── smartTrim.ts │ │ │ ├── useOnOpenBrowserSearch.ts │ │ │ ├── mergeDeep.ts │ │ │ ├── getRefFromDeps.ts │ │ │ ├── generateUniqDefaultName.ts │ │ │ ├── useBackgroundColor.ts │ │ │ └── renderStringWithHighlights.tsx │ │ └── i18n │ │ │ ├── index.tsx │ │ │ └── z.ts │ ├── tss.ts │ ├── main.lazy.tsx │ ├── lazy-icons.ts │ └── Root.tsx ├── public │ ├── preview.png │ ├── fonts │ │ └── WorkSans │ │ │ ├── worksans-bold-webfont.woff2 │ │ │ ├── worksans-medium-webfont.woff2 │ │ │ ├── worksans-regular-webfont.woff2 │ │ │ └── worksans-semibold-webfont.woff2 │ ├── custom-resources │ │ ├── .gitignore │ │ ├── README.md │ │ ├── my-plugin.js │ │ └── my-plugin.ts │ ├── duckdb-extensions │ │ └── v1.4.3 │ │ │ └── wasm_eh │ │ │ ├── json.duckdb_extension.wasm │ │ │ ├── httpfs.duckdb_extension.wasm │ │ │ └── parquet.duckdb_extension.wasm │ └── icons │ │ ├── home.svg │ │ └── files.svg ├── .storybook │ ├── static │ │ └── onyxiaLogo.png │ ├── main.tsx │ └── theme.ts ├── .prettierignore ├── CONTRIBUTING.md ├── scripts │ └── tsconfig.json ├── .prettierrc.json ├── tsconfig.node.json ├── .dockerignore ├── .gitignore ├── tsconfig.json └── README.md ├── .gitignore ├── .sonarcloud.properties ├── .gitmodules ├── ROADMAP.md ├── .github └── workflows │ └── dispatch_on_new_release.yaml └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * !text !filter !merge !diff 2 | -------------------------------------------------------------------------------- /helm-chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Enjoy Onyxia :) -------------------------------------------------------------------------------- /web/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | cd web 2 | npx lint-staged -v -------------------------------------------------------------------------------- /web/src/keycloak-theme/email/theme.properties: -------------------------------------------------------------------------------- 1 | parent=base -------------------------------------------------------------------------------- /web/src/core/adapters/oidc/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./oidc"; 2 | -------------------------------------------------------------------------------- /web/src/ui/App/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Header"; 2 | -------------------------------------------------------------------------------- /web/src/core/adapters/onyxiaApi/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./onyxiaApi"; 2 | -------------------------------------------------------------------------------- /web/src/core/adapters/s3Client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./s3Client"; 2 | -------------------------------------------------------------------------------- /web/src/core/adapters/sqlOlap/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./sqlOlap"; 2 | -------------------------------------------------------------------------------- /web/src/core/tools/ArrayOrNot.ts: -------------------------------------------------------------------------------- 1 | export type ArrayOrNot = T | T[]; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/Quotas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Quotas"; 2 | -------------------------------------------------------------------------------- /web/src/core/adapters/secretManager/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./secretManager"; 2 | -------------------------------------------------------------------------------- /web/src/ui/App/index.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "./App"; 2 | export default App; 3 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Explorer"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /ui 3 | /onyxia-ui 4 | /cra-envs 5 | /screen-scaler 6 | /oidc-spa -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/LauncherDialogs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./LauncherDialogs"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/RootFormComponent/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RootFormComponent"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/MySecretsEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MySecretsEditor"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/SecretsExplorer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SecretsExplorer"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesCards/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MyServicesCards"; 2 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/CodeTextEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CodeTextEditor"; 2 | -------------------------------------------------------------------------------- /web/src/ui/theme/targetWindowInnerWidth.ts: -------------------------------------------------------------------------------- 1 | export const targetWindowInnerWidth = 1980; 2 | -------------------------------------------------------------------------------- /web/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/preview.png -------------------------------------------------------------------------------- /web/src/ui/App/logoContainerWidthInPercent.ts: -------------------------------------------------------------------------------- 1 | export const logoContainerWidthInPercent = 4; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerItems/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExplorerItems"; 2 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.cpd.exclusions=/web/src/ui/i18n/resources/*,**/*.test.ts,**/*.stories.* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "api"] 2 | path = api 3 | url = https://github.com/InseeFrLab/onyxia-api.git 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesCards/MyServicesCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MyServicesCard"; 2 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/computeRootForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./computeRootForm"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerUploadModal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExplorerUploadModal"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/SecretsExplorer/SecretsExplorerItems/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SecretsExplorerItems"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesCards/MyServicesCard/ReadmeDialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ReadmeDialog"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesRestorableConfigs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MyServicesRestorableConfigs"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/ProjectSettingsS3ConfigTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProjectSettingsS3ConfigTab"; 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/ProjectSettingsS3ConfigTab/S3ConfigDialogs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./S3ConfigDialogs"; 2 | -------------------------------------------------------------------------------- /web/.storybook/static/onyxiaLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/.storybook/static/onyxiaLogo.png -------------------------------------------------------------------------------- /web/src/core/usecases/podLogs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/viewQuotas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/fileExplorer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/projectManagement/decoupledLogic/projectConfigsMigration/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./projectConfigsMigration"; 2 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3CodeSnippets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/userProfileForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/img/ProConnect_dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/src/ui/assets/img/ProConnect_dark.webp -------------------------------------------------------------------------------- /web/src/core/usecases/autoLogoutCountdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/mutateHelmValues/mutateHelmValues_update/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mutateHelmValues_update"; 2 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigCreation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/serviceManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/userAuthentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/img/ProConnect_light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/src/ui/assets/img/ProConnect_light.webp -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesRestorableConfigs/MyServicesRestorableConfig/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MyServicesRestorableConfig"; 2 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/shared/validateValueAgainstJSONSchema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./validateValueAgainstJSONSchema"; 2 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigConnectionTest/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/deploymentRegionManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/img/ProConnect_dark_hover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/src/ui/assets/img/ProConnect_dark_hover.webp -------------------------------------------------------------------------------- /web/src/ui/assets/img/ProConnect_light_hover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/src/ui/assets/img/ProConnect_light_hover.webp -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/RootFormComponent/FormFieldGroupComponent/index.tsx: -------------------------------------------------------------------------------- 1 | export { FormFieldGroupComponent } from "./FormFieldGroupComponent"; 2 | -------------------------------------------------------------------------------- /web/src/ui/tools/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | export const copyToClipboard = async (str: string) => { 2 | await navigator.clipboard.writeText(str); 3 | }; 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/catalog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/src/ui/pages/account/AccountProfileTab/index.ts: -------------------------------------------------------------------------------- 1 | import { AccountProfileTab } from "./AccountProfileTab"; 2 | 3 | export default AccountProfileTab; 4 | -------------------------------------------------------------------------------- /web/public/fonts/WorkSans/worksans-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/fonts/WorkSans/worksans-bold-webfont.woff2 -------------------------------------------------------------------------------- /web/public/fonts/WorkSans/worksans-medium-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/fonts/WorkSans/worksans-medium-webfont.woff2 -------------------------------------------------------------------------------- /web/src/core/usecases/dataExplorer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/public/custom-resources/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # But not these files... 5 | !.gitignore 6 | !README.md 7 | !my-plugin.js 8 | !my-plugin.ts -------------------------------------------------------------------------------- /web/public/fonts/WorkSans/worksans-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/fonts/WorkSans/worksans-regular-webfont.woff2 -------------------------------------------------------------------------------- /web/public/fonts/WorkSans/worksans-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/fonts/WorkSans/worksans-semibold-webfont.woff2 -------------------------------------------------------------------------------- /web/src/core/usecases/dataCollection/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/src/core/usecases/serviceDetails/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/src/ui/pages/dataCollection/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = lazy(() => import("./Page")); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/home/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/tabIds.ts: -------------------------------------------------------------------------------- 1 | export const tabIds = ["s3-configs", "security-info"] as const; 2 | 3 | export type TabId = (typeof tabIds)[number]; 4 | -------------------------------------------------------------------------------- /web/src/core/tools/DeepPartial.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = T extends object 2 | ? { 3 | [P in keyof T]?: DeepPartial; 4 | } 5 | : T; 6 | -------------------------------------------------------------------------------- /web/src/core/usecases/clusterEventsMonitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./thunks"; 3 | export * from "./selectors"; 4 | export * from "./evt"; 5 | -------------------------------------------------------------------------------- /web/src/ui/pages/account/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/catalog/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/document/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/myService/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/page404/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | email: string; 3 | familyName?: string; 4 | firstName?: string; 5 | username: string; 6 | }; 7 | -------------------------------------------------------------------------------- /web/src/ui/pages/dataExplorer/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/sqlOlapShell/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/src/core/tools/Omit.ts: -------------------------------------------------------------------------------- 1 | // TODO: Move in tsafe 2 | export type Omit, K extends keyof T> = { 3 | [P in Exclude]: T[P]; 4 | }; 5 | -------------------------------------------------------------------------------- /web/src/core/usecases/sqlOlapShell/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./thunks"; 2 | export const name = "sqlOlapShell"; 3 | export const reducer = null; 4 | export const selectors = {}; 5 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerIcon/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExplorerIcon"; 2 | 3 | export { type SupportedIconsIds, getIconIdFromExtension } from "./icons"; 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | export const LazyComponent = memo(lazy(() => import("./Page"))); 4 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /CHANGELOG.md 3 | /.yarn_home/ 4 | /build/ 5 | /public/ 6 | /build_keycloak/ 7 | /helm-chart/ 8 | /src/vite-env.d.ts 9 | *.gen.ts 10 | 11 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorerEntry/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy, memo } from "react"; 2 | export * from "./route"; 3 | 4 | export const LazyComponent = memo(lazy(() => import("./Page"))); 5 | -------------------------------------------------------------------------------- /web/src/ui/tools/isStorybook.ts: -------------------------------------------------------------------------------- 1 | export const isStorybook = 2 | typeof window === "object" && 3 | Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined; 4 | -------------------------------------------------------------------------------- /web/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Looking to contribute? Thank you! PR are more than welcome. 2 | 3 | To get started, please refers to [the technical documentation](https://docs.onyxia.sh/contributing). 4 | -------------------------------------------------------------------------------- /web/public/duckdb-extensions/v1.4.3/wasm_eh/json.duckdb_extension.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/duckdb-extensions/v1.4.3/wasm_eh/json.duckdb_extension.wasm -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/duckdb-extensions/v1.4.3/wasm_eh/httpfs.duckdb_extension.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/duckdb-extensions/v1.4.3/wasm_eh/httpfs.duckdb_extension.wasm -------------------------------------------------------------------------------- /web/public/duckdb-extensions/v1.4.3/wasm_eh/parquet.duckdb_extension.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia/HEAD/web/public/duckdb-extensions/v1.4.3/wasm_eh/parquet.duckdb_extension.wasm -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/computeRootForm/mergeRangeSliders/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mergeRangeSliders"; 2 | export { createTemporaryRangeSlider } from "./temporaryRangeSlider"; 3 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/html.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/Project.ts: -------------------------------------------------------------------------------- 1 | export type Project = { 2 | id: string; 3 | name: string; 4 | group: string | undefined; 5 | namespace: string; 6 | vaultTopDir: string; 7 | }; 8 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | export type { S3Config } from "./decoupledLogic/getS3Configs"; 5 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node"], 4 | "module": "ES2020", 5 | "moduleResolution": "bundler" 6 | }, 7 | "include": ["./*"] 8 | } 9 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/email/text/email-verification.ftl: -------------------------------------------------------------------------------- 1 | <#ftl output_format="plainText"> 2 | Onyxia: 3 | ${msg("emailVerificationBody",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))} -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/shared/tools.ts: -------------------------------------------------------------------------------- 1 | import type { DirectoryItem, Item } from "./types"; 2 | 3 | export const isDirectory = (item: Item): item is DirectoryItem => 4 | item.kind === "directory"; 5 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/Chart.ts: -------------------------------------------------------------------------------- 1 | export type Chart = { 2 | name: string; 3 | description: string | undefined; 4 | iconUrl: string | undefined; 5 | projectHomepageUrl: string | undefined; 6 | }; 7 | -------------------------------------------------------------------------------- /web/src/core/usecases/projectManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./selectors"; 2 | export * from "./thunks"; 3 | export * from "./state"; 4 | export type { ProjectConfigs } from "./decoupledLogic/ProjectConfigs"; 5 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/ArrowLeft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/mutateHelmValues/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mutateHelmValues_update"; 2 | export * from "./mutateHelmValues_removeArrayItem"; 3 | export * from "./mutateHelmValues_addArrayItem"; 4 | -------------------------------------------------------------------------------- /web/src/ui/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./theme"; 2 | export { injectCustomFontFaceIfNotAlreadyDone } from "./injectCustomFontFaceIfNotAlreadyDone"; 3 | export { targetWindowInnerWidth } from "./targetWindowInnerWidth"; 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/ArrowBottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/ArrowTop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/markdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/page404/route.ts: -------------------------------------------------------------------------------- 1 | import { createGroup, defineRoute } from "type-route"; 2 | 3 | export const routeDefs = { 4 | page404: defineRoute("/404") 5 | }; 6 | 7 | export const routeGroup = createGroup(routeDefs); 8 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/ArrowRight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/shared/formattedDate/index.ts: -------------------------------------------------------------------------------- 1 | export { useFormattedDate, useFromNow } from "./useFormattedDate"; 2 | export { getFormattedDate } from "./getFormattedDate"; 3 | export { fromNow, formatDuration } from "./dateTimeFormatter"; 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/email/html/email-verification.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Onyxia: 4 | ${kcSanitize(msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc} 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/home/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | home: defineRoute(["/", "/home", "/accueil"]) 5 | }; 6 | 7 | export const routeGroup = createGroup(routeDefs); 8 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/i18n/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | export * from "./i18n"; 3 | 4 | export { languages, fallbackLanguage, type Language } from "./types"; 5 | export { zLanguage, zLocalizedString } from "./z"; 6 | -------------------------------------------------------------------------------- /web/src/ui/pages/sqlOlapShell/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | sqlOlapShell: defineRoute("/sql-olap-shell") 5 | }; 6 | 7 | export const routeGroup = createGroup(routeDefs); 8 | -------------------------------------------------------------------------------- /web/src/core/tools/generateRandomPassword.ts: -------------------------------------------------------------------------------- 1 | export function generateRandomPassword() { 2 | return Array(2) 3 | .fill("") 4 | .map(() => Math.random().toString(36).slice(-10)) 5 | .join("") 6 | .replace(/\./g, ""); 7 | } 8 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/HomeIconStock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | 2 | What we're working on is listed as [Github Milestones](https://github.com/InseeFrLab/onyxia/milestones). 3 | Do not hesitate to give feedback and to vote for the features that are more important to you. 4 | We prioritize based on community feedback! -------------------------------------------------------------------------------- /web/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/CheckboxEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorerEntry/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | fileExplorerEntry: defineRoute([`/file-explorer`, `/my-files`]) 5 | }; 6 | 7 | export const routeGroup = createGroup(routeDefs); 8 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/account/accountTabIds.ts: -------------------------------------------------------------------------------- 1 | export const accountTabIds = [ 2 | "profile", 3 | "git", 4 | "storage", 5 | "k8sCodeSnippets", 6 | "vault", 7 | "user-interface" 8 | ] as const; 9 | 10 | export type AccountTabId = (typeof accountTabIds)[number]; 11 | -------------------------------------------------------------------------------- /web/src/ui/theme/emotionCache.ts: -------------------------------------------------------------------------------- 1 | import { createCssAndCx } from "tss-react/cssAndCx"; 2 | import createCache from "@emotion/cache"; 3 | 4 | export const emotionCache = createCache({ 5 | key: "tss" 6 | }); 7 | 8 | export const { css, cx } = createCssAndCx({ cache: emotionCache }); 9 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/css.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/tools/triggerBrowserDonwload.ts: -------------------------------------------------------------------------------- 1 | export function triggerBrowserDownload(params: { url: string; filename: string }) { 2 | const { url, filename } = params; 3 | const a = document.createElement("a"); 4 | a.href = url; 5 | a.download = filename; 6 | a.click(); 7 | } 8 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Launch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/README.md: -------------------------------------------------------------------------------- 1 | This will eventually be externalized as `@onyxia-ui/code-editor` 2 | The repo has already been created: https://github.com/InseeFrLab/onyxia-ui-code-editor 3 | 4 | For now we keep it inside the app for flexibility until we are sure we won't need to change it much. 5 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/CheckboxActive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/csv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/custom-resources/README.md: -------------------------------------------------------------------------------- 1 | # Custom resources directory 2 | 3 | In this directory you can drop the custom resources for your instance. 4 | To use them in production, create a Zip file with the content of 5 | this directory and use the `CUSTOM_RESOURCES` environnement 6 | variable. See `.env` file for more infos. -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/core/tools/timeFormat/type.ts: -------------------------------------------------------------------------------- 1 | import { DURATION_DIVISOR_KEYS } from "./constants"; 2 | 3 | export type DurationDivisorKey = (typeof DURATION_DIVISOR_KEYS)[number]; 4 | 5 | export type DurationTranslationFunction = ( 6 | key: "singular" | "plural", 7 | params: { divisorKey: DurationDivisorKey } 8 | ) => string; 9 | -------------------------------------------------------------------------------- /web/src/ui/tools/areSameSet.ts: -------------------------------------------------------------------------------- 1 | export function areSameSet(set1: Set, set2: Set) { 2 | if (set1.size !== set2.size) { 3 | return false; 4 | } 5 | for (const elem of set1) { 6 | if (!set2.has(elem)) { 7 | return false; 8 | } 9 | } 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /web/src/ui/tools/JSONSortStringify.ts: -------------------------------------------------------------------------------- 1 | import "minimal-polyfills/Object.fromEntries"; 2 | 3 | export function JSONSortStringify(record: Record) { 4 | return JSON.stringify( 5 | Object.fromEntries( 6 | Object.entries(record).sort(([key1], [key2]) => (key1 < key2 ? 1 : -1)) 7 | ) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Build.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/yml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/tools/getIsJSON5ObjectOrArray.ts: -------------------------------------------------------------------------------- 1 | import JSON5 from "json5"; 2 | 3 | export function getIsJSON5ObjectOrArray(str: string): boolean { 4 | let parsed: unknown; 5 | 6 | try { 7 | parsed = JSON5.parse(str); 8 | } catch { 9 | return false; 10 | } 11 | 12 | return typeof parsed === "object" && parsed !== null; 13 | } 14 | -------------------------------------------------------------------------------- /web/src/core/usecases/projectManagement/decoupledLogic/projectVaultTopDirPath_reserved.ts: -------------------------------------------------------------------------------- 1 | import { join as pathJoin } from "pathe"; 2 | 3 | export function getProjectVaultTopDirPath_reserved(params: { 4 | projectVaultTopDirPath: string; 5 | }) { 6 | const { projectVaultTopDirPath } = params; 7 | 8 | return pathJoin(projectVaultTopDirPath, ".onyxia"); 9 | } 10 | -------------------------------------------------------------------------------- /web/src/tss.ts: -------------------------------------------------------------------------------- 1 | import { createTss } from "tss-react"; 2 | import { useTheme } from "onyxia-ui"; 3 | import type { Theme } from "ui/theme"; 4 | 5 | export const { tss } = createTss({ 6 | useContext: function useContext() { 7 | const theme = useTheme(); 8 | return { theme }; 9 | } 10 | }); 11 | 12 | export const useStyles = tss.create({}); 13 | -------------------------------------------------------------------------------- /web/src/ui/pages/dataCollection/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | dataCollection: defineRoute( 5 | { 6 | source: param.query.optional.string 7 | }, 8 | () => `/data-collection` 9 | ) 10 | }; 11 | 12 | export const routeGroup = createGroup(routeDefs); 13 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/Catalog.ts: -------------------------------------------------------------------------------- 1 | import type { LocalizedString } from "./Language"; 2 | 3 | export type Catalog = { 4 | id: string; 5 | name: LocalizedString; 6 | repositoryUrl: string; 7 | description: LocalizedString | undefined; 8 | isProduction: boolean; 9 | visibility: "always" | "ony in personal projects" | "only in groups projects"; 10 | }; 11 | -------------------------------------------------------------------------------- /web/src/core/usecases/restorableConfigManagement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | export * from "./selectors"; 3 | export * from "./thunks"; 4 | export { getAreSameRestorableConfig } from "./decoupledLogic/getAreSameRestorableConfig"; 5 | export { 6 | type RestorableServiceConfigRef, 7 | getAreSameRestorableConfigRef 8 | } from "./decoupledLogic/getAreSameRestorableConfigRef"; 9 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/OidcParams.ts: -------------------------------------------------------------------------------- 1 | export type OidcParams = { 2 | issuerUri: string; 3 | clientId: string; 4 | extraQueryParams_raw: string | undefined; 5 | scope_spaceSeparated: string | undefined; 6 | idleSessionLifetimeInSeconds: number | undefined; 7 | }; 8 | 9 | export type OidcParams_Partial = { [P in keyof OidcParams]: OidcParams[P] | undefined }; 10 | -------------------------------------------------------------------------------- /web/src/ui/tools/getBrowser.ts: -------------------------------------------------------------------------------- 1 | import memoize from "memoizee"; 2 | 3 | export const getBrowser = memoize((): "chrome" | "safari" | "firefox" | undefined => { 4 | const { userAgent } = navigator; 5 | 6 | for (const id of ["chrome", "safari", "firefox"] as const) { 7 | if (new RegExp(id, "i").test(userAgent)) { 8 | return id; 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /web/src/core/tools/Stringifyable/getIsAtomic.ts: -------------------------------------------------------------------------------- 1 | import type { Stringifyable, StringifyableAtomic } from "./Stringifyable"; 2 | 3 | export function getIsAtomic( 4 | stringifyable: Stringifyable 5 | ): stringifyable is StringifyableAtomic { 6 | return ( 7 | ["string", "number", "boolean"].includes(typeof stringifyable) || 8 | stringifyable === null 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/lua.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/myService/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | myService: defineRoute( 5 | { 6 | helmReleaseName: param.path.string 7 | }, 8 | ({ helmReleaseName }) => `/my-service/${helmReleaseName}` 9 | ) 10 | }; 11 | 12 | export const routeGroup = createGroup(routeDefs); 13 | -------------------------------------------------------------------------------- /web/src/core/tools/fnv1aHashToHex.ts: -------------------------------------------------------------------------------- 1 | export function fnv1aHashToHex(str: string) { 2 | let hash = 2166136261; 3 | for (let i = 0; i < str.length; i++) { 4 | hash ^= str.charCodeAt(i); 5 | hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); 6 | } 7 | return (hash >>> 0).toString(16); // Convert to unsigned 32-bit integer and then to hexadecimal 8 | } 9 | -------------------------------------------------------------------------------- /web/src/ui/shared/enforceLogin.ts: -------------------------------------------------------------------------------- 1 | import { getCore } from "core"; 2 | 3 | export async function enforceLogin(): Promise { 4 | const core = await getCore(); 5 | 6 | if (!core.states.userAuthentication.getMain().isUserLoggedIn) { 7 | await core.functions.userAuthentication.login({ 8 | doesCurrentHrefRequiresAuth: true 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/src/ui/tools/simpleHash.ts: -------------------------------------------------------------------------------- 1 | export function simpleHash(str: string) { 2 | let hash = 2166136261; // FNV offset basis 3 | for (let i = 0; i < str.length; i++) { 4 | hash ^= str.charCodeAt(i); 5 | hash *= 16777619; // FNV prime 6 | } 7 | // Use bitwise XOR for converting hash to 32-bit integer, then convert to string 8 | return (hash >>> 0).toString(); 9 | } 10 | -------------------------------------------------------------------------------- /web/src/core/tools/Stringifyable/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Stringifyable"; 2 | export { assignValueAtPath } from "./assignValueAtPath"; 3 | export { getIsAtomic } from "./getIsAtomic"; 4 | export { getValueAtPath } from "./getValueAtPath"; 5 | export { applyDiffPatch } from "./applyDiffPatch"; 6 | export { computeDiff } from "./computeDiff"; 7 | export { getDoesPathStartWith } from "./getDoesPathStartWith"; 8 | -------------------------------------------------------------------------------- /web/src/ui/pages/page404/Page404.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import Page404 from "./Page"; 3 | 4 | const meta = { 5 | title: "Pages/Page404", 6 | component: Page404 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {} 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/tss.ts: -------------------------------------------------------------------------------- 1 | import { createTss } from "tss-react"; 2 | import { useTheme } from "onyxia-ui"; 3 | import type { Theme } from "onyxia-ui/lib/theme"; 4 | 5 | export const { tss } = createTss({ 6 | useContext: function useContext() { 7 | const theme = useTheme(); 8 | return { theme }; 9 | } 10 | }); 11 | 12 | export const useStyles = tss.create({}); 13 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/More.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/xml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helm-chart/templates/web/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.web.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "onyxia.web.serviceAccountName" . }} 6 | labels: 7 | {{- include "onyxia.web.labels" . | nindent 4 }} 8 | {{- with .Values.web.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /web/src/ui/shared/LoadingDots.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { LoadingDots } from "./LoadingDots"; 3 | 4 | const meta = { 5 | title: "Shared/LoadingDots", 6 | component: LoadingDots 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {} 15 | }; 16 | -------------------------------------------------------------------------------- /helm-chart/templates/api/serviceaccount-api.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.api.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "onyxia.api.serviceAccountName" . }} 6 | labels: 7 | {{- include "onyxia.api.labels" . | nindent 4 }} 8 | {{- with .Values.api.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/directory.svg: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, createGroup, param } from "type-route"; 2 | 3 | export const routeDefs = { 4 | mySecrets: defineRoute( 5 | { 6 | path: param.path.trailing.optional.string, 7 | openFile: param.query.optional.string 8 | }, 9 | ({ path }) => `/my-secrets/${path}` 10 | ) 11 | }; 12 | 13 | export const routeGroup = createGroup(routeDefs); 14 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/index.ts: -------------------------------------------------------------------------------- 1 | export { computeHelmValues } from "./computeHelmValues"; 2 | export { computeRootForm } from "./computeRootForm"; 3 | export { 4 | mutateHelmValues_update, 5 | mutateHelmValues_addArrayItem, 6 | mutateHelmValues_removeArrayItem 7 | } from "./mutateHelmValues"; 8 | export { computeAutocompleteOptions } from "./computeAutocompleteOptions"; 9 | export type * from "./formTypes"; 10 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/catalog/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | catalog: defineRoute( 5 | { 6 | catalogId: param.path.optional.string, 7 | search: param.query.optional.string.default("") 8 | }, 9 | ({ catalogId }) => `/catalog/${catalogId}` 10 | ) 11 | }; 12 | 13 | export const routeGroup = createGroup(routeDefs); 14 | -------------------------------------------------------------------------------- /web/src/ui/shared/MaybeLink.tsx: -------------------------------------------------------------------------------- 1 | import MuiLink from "@mui/material/Link"; 2 | 3 | export function MaybeLink(props: { 4 | href: string | undefined; 5 | children: React.ReactNode; 6 | }) { 7 | const { href } = props; 8 | return href === undefined ? ( 9 | <>{props.children} 10 | ) : ( 11 | 12 | {props.children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/core/tools/Stringifyable/getDoesPathStartWith.ts: -------------------------------------------------------------------------------- 1 | export function getDoesPathStartWith(params: { 2 | shorterPath: (string | number)[]; 3 | longerPath: (string | number)[]; 4 | }) { 5 | const { shorterPath, longerPath } = params; 6 | 7 | for (let i = 0; i < shorterPath.length; i++) { 8 | if (shorterPath[i] !== longerPath[i]) { 9 | return false; 10 | } 11 | } 12 | 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/singlePackage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, createGroup } from "type-route"; 2 | 3 | export const routeDefs = { 4 | myServices: defineRoute( 5 | { 6 | isSavedConfigsExtended: param.query.optional.boolean.default(false), 7 | autoOpenHelmReleaseName: param.query.optional.string 8 | }, 9 | () => `/my-services` 10 | ) 11 | }; 12 | 13 | export const routeGroup = createGroup(routeDefs); 14 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helm-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm-chart/templates/api/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "onyxia.api.fullname" . }} 5 | labels: 6 | {{- include "onyxia.api.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.api.service.type }} 9 | ports: 10 | - port: {{ .Values.api.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "onyxia.api.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm-chart/templates/web/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "onyxia.web.fullname" . }} 5 | labels: 6 | {{- include "onyxia.web.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.web.service.type }} 9 | ports: 10 | - port: {{ .Values.web.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "onyxia.web.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /web/src/ui/shared/PortraitModeUnsupported.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { PortraitModeUnsupported } from "./PortraitModeUnsupported"; 3 | 4 | const meta = { 5 | title: "Shared/PortraitModeUnsupported", 6 | component: PortraitModeUnsupported 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {} 15 | }; 16 | -------------------------------------------------------------------------------- /helm-chart/templates/tests/test-connection-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "onyxia.api.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "onyxia.api.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "onyxia.api.fullname" . }}:{{ .Values.api.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /helm-chart/templates/tests/test-connection-ihm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "onyxia.web.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "onyxia.web.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "onyxia.web.fullname" . }}:{{ .Values.web.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/shared/types.ts: -------------------------------------------------------------------------------- 1 | import type { CurrentWorkingDirectoryView } from "core/usecases/fileExplorer"; 2 | 3 | export type Item = CurrentWorkingDirectoryView.Item; 4 | export type DirectoryItem = CurrentWorkingDirectoryView.Item.Directory; 5 | export type FileItem = CurrentWorkingDirectoryView.Item.File; 6 | 7 | export const viewModes = ["list", "block"] as const; //the order is important, first is the default for the router 8 | 9 | export type ViewMode = (typeof viewModes)[number]; 10 | -------------------------------------------------------------------------------- /web/src/ui/tools/parseCssSpacing.ts: -------------------------------------------------------------------------------- 1 | export function parseCssSpacing(value: string): `${string}${"px" | "em" | "rem" | "%"}` { 2 | if (!/^-?[0-9]+(?:\.[0.9]+)?(?:(?:px)|(?:em)|(?:rem)|%)?$/.test(value)) { 3 | throw new Error( 4 | `CSS Spacing Malformed. Example of valid value: 10 or -10 or 10px or 1.5em or -1.5rem or 10%. Got: ${value}` 5 | ); 6 | } 7 | 8 | if (/[0-9]$/.test(value)) { 9 | return `${value}px`; 10 | } 11 | 12 | return value as any; 13 | } 14 | -------------------------------------------------------------------------------- /helm-chart/templates/onboarding/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.onboarding.enabled .Values.onboarding.clusterRole.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ .Values.onboarding.clusterRole.name | default (include "onyxia.onboarding.fullname" .) }} 6 | rules: 7 | {{- range .Values.onboarding.clusterRole.rules }} 8 | - apiGroups: {{ .apiGroups | toJson }} 9 | resources: {{ .resources | toJson }} 10 | verbs: {{ .verbs | toJson }} 11 | {{- end }} 12 | {{- end }} -------------------------------------------------------------------------------- /web/src/core/tools/timeFormat/constants.ts: -------------------------------------------------------------------------------- 1 | const SECOND = 1000; 2 | 3 | export const TIME_UNITS = { 4 | SECOND, 5 | MINUTE: 60 * SECOND, 6 | HOUR: 60 * 60 * SECOND, 7 | DAY: 24 * 60 * 60 * SECOND, 8 | WEEK: 7 * 24 * 60 * 60 * SECOND, 9 | MONTH: 30 * 24 * 60 * 60 * SECOND, 10 | YEAR: 365 * 24 * 60 * 60 * SECOND 11 | }; 12 | 13 | export const DURATION_DIVISOR_KEYS = [ 14 | "second", 15 | "minute", 16 | "hour", 17 | "day", 18 | "week", 19 | "month", 20 | "year" 21 | ] as const; 22 | -------------------------------------------------------------------------------- /web/src/core/usecases/autoLogoutCountdown/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { State as RootState } from "core/bootstrap"; 2 | import { name } from "./state"; 3 | import { createSelector } from "clean-architecture"; 4 | 5 | const state = (rootState: RootState) => rootState[name]; 6 | 7 | const secondsLeft = createSelector(state, state => state.secondsLeft); 8 | 9 | const main = createSelector(secondsLeft, secondsLeft => ({ secondsLeft })); 10 | 11 | export const selectors = { main }; 12 | 13 | export const privateSelectors = { secondsLeft }; 14 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/login/KcContext.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendKcContext } from "keycloakify/login"; 2 | import type { KcEnvName, ThemeName } from "../kc.gen"; 3 | 4 | export type KcContextExtension = { 5 | themeName: ThemeName; 6 | properties: Record; 7 | darkMode?: boolean; 8 | }; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 11 | export type KcContextExtensionPerPage = {}; 12 | 13 | export type KcContext = ExtendKcContext; 14 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorerEntry/FileExplorerDisabledDialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { FileExplorerDisabledDialog } from "./FileExplorerDisabledDialog"; 3 | 4 | const meta = { 5 | title: "Pages/MyFiles/FileExplorerDisabledDialog", 6 | component: FileExplorerDisabledDialog 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {} 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/ui/shared/RoundLogo.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { RoundLogo } from "./RoundLogo"; 3 | 4 | const meta = { 5 | title: "Shared/RoundLogo", 6 | component: RoundLogo 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | url: "https://minio.lab.sspcloud.fr/projet-onyxia/assets/servicesImg/vscode.png", 16 | size: "default" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /web/src/ui/tools/emailRegExp.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-control-regex */ 2 | export const emailRegExp = 3 | /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/; 4 | -------------------------------------------------------------------------------- /web/src/ui/tools/getPathDepth.ts: -------------------------------------------------------------------------------- 1 | //TODO: Remove this util when we migrate to new new explorer. 2 | 3 | /** 4 | / -> 0 5 | /a -> 1 6 | /a/ -> 1 7 | /a/b -> 2 8 | 9 | 10 | . -> 0 11 | ./ -> 0 12 | ./a -> 1 13 | ./a/ -> 1 14 | ./a/b -> 2 15 | 16 | a -> 1 17 | a/ 18 | a/b 19 | a/b/ 20 | 21 | */ 22 | export function getPathDepth(path: string): number { 23 | return path 24 | .replace(/^\./, "") 25 | .replace(/^\//, "") 26 | .replace(/\/$/, "") 27 | .split("/") 28 | .filter(s => s !== "").length; 29 | } 30 | -------------------------------------------------------------------------------- /web/src/core/tools/OptionalIfCanBeUndefined.ts: -------------------------------------------------------------------------------- 1 | type PropertiesThatCanBeUndefined> = { 2 | [Key in keyof T]: undefined extends T[Key] ? Key : never; 3 | }[keyof T]; 4 | 5 | /** 6 | * OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }> 7 | * is 8 | * { p1?: string | undefined; p2: string } 9 | */ 10 | export type OptionalIfCanBeUndefined> = { 11 | [K in PropertiesThatCanBeUndefined]?: T[K]; 12 | } & { [K in Exclude>]: T[K] }; 13 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigConnectionTest/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { State as RootState } from "core/bootstrap"; 2 | import { createSelector } from "clean-architecture"; 3 | import { name } from "./state"; 4 | 5 | const state = (rootState: RootState) => rootState[name]; 6 | 7 | const configTestResults = createSelector(state, state => state.configTestResults); 8 | const ongoingConfigTests = createSelector(state, state => state.ongoingConfigTests); 9 | 10 | export const protectedSelectors = { 11 | configTestResults, 12 | ongoingConfigTests 13 | }; 14 | -------------------------------------------------------------------------------- /helm-chart/templates/onboarding/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.onboarding.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "onyxia.onboarding.fullname" . }} 6 | labels: 7 | {{- include "onyxia.onboarding.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.onboarding.service.type }} 10 | ports: 11 | - port: {{ .Values.onboarding.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "onyxia.onboarding.selectorLabels" . | nindent 4 }} 17 | {{- end }} -------------------------------------------------------------------------------- /helm-chart/templates/onboarding/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.onboarding.enabled .Values.onboarding.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "onyxia.onboarding.serviceAccountName" . }} 6 | labels: 7 | {{- include "onyxia.onboarding.labels" . | nindent 4 }} 8 | {{- with .Values.onboarding.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.onboarding.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /web/src/ui/shared/CircularMetric.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CircularMetric } from "./CircularMetric"; 3 | 4 | console.log(import.meta.env); 5 | const meta = { 6 | title: "Shared/CircularMetric", 7 | component: CircularMetric 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | percentage: 50, 17 | severity: "info", 18 | children: null 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /web/public/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/shared/CopyToClipboardIconButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CopyToClipboardIconButton } from "./CopyToClipboardIconButton"; 3 | 4 | const meta = { 5 | title: "Shared/CopyToClipboardIconButton", 6 | component: CopyToClipboardIconButton 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | textToCopy: "This is the default text to copy!" 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /web/src/main.lazy.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { Root } from "./Root"; 4 | 5 | { 6 | const version = import.meta.env.WEB_VERSION; 7 | 8 | console.log( 9 | [ 10 | `Docker image inseefrlab/onyxia-web version: ${version}`, 11 | `https://github.com/InseeFrLab/onyxia/tree/web-v${version}/web` 12 | ].join("\n") 13 | ); 14 | } 15 | 16 | createRoot(document.getElementById("root")!).render( 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /web/src/ui/pages/dataExplorer/UrlInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { UrlInput } from "./UrlInput"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/DataExplorer/UrlInput", 7 | component: UrlInput 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | url: "https://example.com", 17 | onUrlChange: action("URL changed") 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/java.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/shared/BrandHeaderSection.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { BrandHeaderSection } from "./BrandHeaderSection"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Shared/BrandHeaderSection", 7 | component: BrandHeaderSection 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | doShowOnyxia: true, 17 | link: { href: "", onClick: action("header") } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /helm-chart/templates/api/rolebinding-namespace-admin.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.api.serviceAccount.create -}} 2 | {{- if not .Values.api.serviceAccount.clusterAdmin -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: RoleBinding 5 | metadata: 6 | name: {{ include "onyxia.fullname" . }} 7 | labels: 8 | {{- include "onyxia.api.labels" . | nindent 4 }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: admin 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "onyxia.api.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | {{- end -}} -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/scala.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/shared/formattedDate/getFormattedDate.ts: -------------------------------------------------------------------------------- 1 | export function getFormattedDate(params: { time: number; lang: string }): string { 2 | const { time, lang } = params; 3 | 4 | const date = new Date(time); 5 | 6 | const isSameYear = date.getFullYear() === new Date().getFullYear(); 7 | 8 | const formattedDate = new Intl.DateTimeFormat(lang, { 9 | weekday: "long", 10 | day: "numeric", 11 | month: "long", 12 | year: isSameYear ? undefined : "numeric", 13 | hour: "numeric", 14 | minute: "numeric" 15 | }).format(date); 16 | 17 | return formattedDate; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/ui/shared/SettingSectionHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { SettingSectionHeader } from "./SettingSectionHeader"; 3 | 4 | const meta = { 5 | title: "Shared/SettingSectionHeader", 6 | component: SettingSectionHeader 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | title: "Section Title", 16 | helperText: "This is some additional information.", 17 | tooltipText: "More details about this section" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /helm-chart/templates/api/configmaps-userprofile.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.api.userProfile.enabled }} 2 | {{- $fullname := include "onyxia.fullname" . -}} 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: {{ $fullname }}-userprofile 7 | data: 8 | default: |- 9 | {{ .Values.api.userProfile.default.profileSchema | indent 4 }} 10 | --- 11 | {{- range .Values.api.userProfile.roles }} 12 | {{- $role := .roleName -}} 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: {{ $fullname }}-{{ printf "%s %s" "role" $role | sha256sum | trunc 8 }} 17 | data: |- 18 | {{ .profileSchema | indent 4 }} 19 | --- 20 | {{- end }} 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /web/.storybook/main.tsx: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | export default { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | 6 | addons: [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "storybook-dark-mode", 10 | "@storybook/addon-interactions", 11 | "@storybook/addon-themes", 12 | "@storybook/addon-a11y" 13 | ], 14 | 15 | framework: { 16 | name: "@storybook/react-vite", 17 | options: {} 18 | }, 19 | 20 | staticDirs: ["./static", "../public"] 21 | } as StorybookConfig; 22 | -------------------------------------------------------------------------------- /web/src/ui/pages/catalog/CatalogNoSearchMatches.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CatalogNoSearchMatches } from "./CatalogNoSearchMatches"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/Catalog/CatalogNoSearchMatches", 7 | component: CatalogNoSearchMatches 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | search: "example service", 17 | onGoBackClick: action("Go back clicked") 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /web/src/core/usecases/userAuthentication/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from "clean-architecture"; 2 | import { type State as RootState } from "core/bootstrap"; 3 | import { name } from "./state"; 4 | 5 | const state = (rootState: RootState) => rootState[name]; 6 | 7 | const main = createSelector(state, state => { 8 | if (!state.isUserLoggedIn) { 9 | return { isUserLoggedIn: false as const }; 10 | } 11 | 12 | const { user, isKeycloak } = state; 13 | 14 | return { 15 | isUserLoggedIn: true as const, 16 | user, 17 | isKeycloak 18 | }; 19 | }); 20 | 21 | export const selectors = { 22 | main 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/illustrator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/tools/use.ts: -------------------------------------------------------------------------------- 1 | const valueByPromise = new WeakMap, unknown>(); 2 | const setOfProcessedPromises = new WeakSet>(); 3 | 4 | /** Polyfill of React 19's use hook (only used against promises) */ 5 | export function use(promise: Promise): T { 6 | if (!valueByPromise.has(promise)) { 7 | if (!setOfProcessedPromises.has(promise)) { 8 | setOfProcessedPromises.add(promise); 9 | promise.then(value => valueByPromise.set(promise, value)); 10 | } 11 | throw promise; 12 | } 13 | // @ts-expect-error: We know what we are doing. 14 | return valueByPromise.get(promise); 15 | } 16 | -------------------------------------------------------------------------------- /web/src/core/tools/waitForDebounce.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from "evt/tools/Deferred"; 2 | 3 | export function createWaitForDebounce(params: { delay: number }) { 4 | const { delay } = params; 5 | 6 | let timeout: ReturnType | undefined = undefined; 7 | 8 | function waitForDebounce(): Promise { 9 | if (timeout !== undefined) { 10 | clearTimeout(timeout); 11 | } 12 | 13 | const d = new Deferred(); 14 | 15 | timeout = setTimeout(() => { 16 | d.resolve(); 17 | }, delay); 18 | 19 | return d.pr; 20 | } 21 | 22 | return { waitForDebounce }; 23 | } 24 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/theme/palette.ts: -------------------------------------------------------------------------------- 1 | import { defaultPalette } from "onyxia-ui"; 2 | import { env } from "env"; 3 | import { mergeDeep } from "ui/tools/mergeDeep"; 4 | 5 | export function getPalette(params: { isDarkModeEnabled: boolean }) { 6 | const { isDarkModeEnabled } = params; 7 | 8 | return mergeDeep( 9 | { 10 | ...defaultPalette, 11 | limeGreen: { 12 | main: "#BAFF29", 13 | light: "#E2FFA6" 14 | } 15 | }, 16 | mergeDeep( 17 | env.PALETTE_OVERRIDE, 18 | isDarkModeEnabled ? env.PALETTE_OVERRIDE_DARK : env.PALETTE_OVERRIDE_LIGHT 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /web/src/core/usecases/deploymentRegionManagement/state.ts: -------------------------------------------------------------------------------- 1 | import type { DeploymentRegion } from "core/ports/OnyxiaApi"; 2 | import { 3 | createUsecaseActions, 4 | createObjectThatThrowsIfAccessed 5 | } from "clean-architecture"; 6 | 7 | type State = { 8 | availableDeploymentRegions: DeploymentRegion[]; 9 | currentDeploymentRegionId: string; 10 | }; 11 | 12 | export const name = "deploymentRegionManagement"; 13 | 14 | export const { reducer, actions } = createUsecaseActions({ 15 | name, 16 | initialState: createObjectThatThrowsIfAccessed(), 17 | reducers: { 18 | initialize: (_, { payload }: { payload: State }) => payload 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /web/src/core/tools/Object.fromEntries.ts: -------------------------------------------------------------------------------- 1 | if (!(Object as any).fromEntries) { 2 | Object.defineProperty(Object, "fromEntries", { 3 | value: function (entries: any) { 4 | if (!entries || !entries[Symbol.iterator]) { 5 | throw new Error( 6 | "Object.fromEntries() requires a single iterable argument" 7 | ); 8 | } 9 | 10 | const o: any = {}; 11 | 12 | Object.keys(entries).forEach(key => { 13 | const [k, v] = entries[key]; 14 | 15 | o[k] = v; 16 | }); 17 | 18 | return o; 19 | } 20 | }); 21 | } 22 | 23 | export {}; 24 | -------------------------------------------------------------------------------- /web/src/ui/pages/catalog/CatalogSwitcherButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CatalogSwitcherButton } from "./CatalogSwitcherButton"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/Catalog/CatalogSwitcherButton", 7 | component: CatalogSwitcherButton 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | isSelected: false, 17 | onClick: action("Button clicked"), 18 | text: Interactive services 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /helm-chart/templates/api/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.api.serviceAccount.create -}} 2 | {{- if .Values.api.serviceAccount.clusterAdmin -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: {{ include "onyxia.fullname" . }} 7 | labels: 8 | {{- include "onyxia.api.labels" . | nindent 4 }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: {{ default "cluster-admin" .Values.api.serviceAccount.existingClusterRole }} 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "onyxia.api.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | {{- end -}} -------------------------------------------------------------------------------- /web/.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | 26 | server/*.spec.js 27 | kubernetes 28 | 29 | /dist 30 | /Dockerfile 31 | /node_modules 32 | /.github 33 | /.vscode 34 | /docs 35 | 36 | /.env.local 37 | /.env.local.yaml 38 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/photoshop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/SecretsExplorer/ExplorerIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerIcon } from "./ExplorerIcon"; 3 | 4 | const meta = { 5 | title: "Pages/MySecrets/SecretsExplorer/ExplorerIcon", 6 | component: ExplorerIcon 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const SecretIcon: Story = { 14 | args: { 15 | iconId: "secret", 16 | hasShadow: true 17 | } 18 | }; 19 | 20 | export const DirectoryIcon: Story = { 21 | args: { 22 | iconId: "directory", 23 | hasShadow: false 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/HelmRelease.ts: -------------------------------------------------------------------------------- 1 | export type HelmRelease = { 2 | helmReleaseName: string; 3 | friendlyName: string | undefined; 4 | urls: string[]; 5 | startedAt: number; 6 | postInstallInstructions: string | undefined; 7 | isShared: boolean | undefined; 8 | values: Record; 9 | ownerUsername: string; 10 | appVersion: string; 11 | revision: string; 12 | catalogId: string; 13 | chartName: string; 14 | chartVersion: string; 15 | areAllTasksReady: boolean; 16 | status: "deployed" | "pending-install" | "failed"; 17 | podNames: string[]; 18 | doesSupportSuspend: boolean; 19 | isSuspended: boolean; 20 | }; 21 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/shared/getHelmValuesPathDeeperCommonSubpath.test.ts: -------------------------------------------------------------------------------- 1 | import { symToStr } from "tsafe/symToStr"; 2 | import { it, expect, describe } from "vitest"; 3 | import { getHelmValuesPathDeeperCommonSubpath } from "./getHelmValuesPathDeeperCommonSubpath"; 4 | 5 | describe(symToStr({ getHelmValuesPathDeeperCommonSubpath }), () => { 6 | it("base case", () => { 7 | const got = getHelmValuesPathDeeperCommonSubpath({ 8 | helmValuesPath1: ["a", 1, "b", "c"], 9 | helmValuesPath2: ["a", 1, "d", "e"] 10 | }); 11 | 12 | const expected = ["a", 1]; 13 | 14 | expect(got).toStrictEqual(expected); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /web/src/core/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | In this file we export utilities for using the core in a React setup. 3 | This file is the only place in src/core where it's okay to assume we are 4 | using react. 5 | If we where to change our UI framework we would only update this file to 6 | export an API more adapted to our new front. (But we don't plan to leave React) 7 | */ 8 | 9 | import { createReactApi } from "clean-architecture/react"; 10 | import { bootstrapCore } from "./bootstrap"; 11 | export { type Language, type LocalizedString } from "./ports/OnyxiaApi"; 12 | 13 | export const { triggerCoreBootstrap, useCoreState, getCoreSync, getCore } = 14 | createReactApi({ 15 | bootstrapCore 16 | }); 17 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/shared/getHelmValuesPathDeeperCommonSubpath.ts: -------------------------------------------------------------------------------- 1 | export function getHelmValuesPathDeeperCommonSubpath(params: { 2 | helmValuesPath1: (string | number)[]; 3 | helmValuesPath2: (string | number)[]; 4 | }): (string | number)[] { 5 | const { helmValuesPath1, helmValuesPath2 } = params; 6 | 7 | const helmValuesPath: (string | number)[] = []; 8 | 9 | for (let i = 0; i < Math.min(helmValuesPath1.length, helmValuesPath2.length); i++) { 10 | if (helmValuesPath1[i] !== helmValuesPath2[i]) { 11 | break; 12 | } 13 | 14 | helmValuesPath.push(helmValuesPath1[i]); 15 | } 16 | 17 | return helmValuesPath; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/core/adapters/oidc/mock.ts: -------------------------------------------------------------------------------- 1 | import type { Oidc } from "core/ports/Oidc"; 2 | import { createMockOidc } from "oidc-spa/mock"; 3 | 4 | export async function createOidc(params: { 5 | isUserInitiallyLoggedIn: boolean; 6 | }): Promise { 7 | const { isUserInitiallyLoggedIn } = params; 8 | 9 | const oidc = await createMockOidc({ 10 | isUserInitiallyLoggedIn, 11 | homeUrl: import.meta.env.BASE_URL, 12 | mockedTokens: { 13 | decodedIdToken: {} 14 | } 15 | }); 16 | 17 | if (!oidc.isUserLoggedIn) { 18 | return oidc; 19 | } 20 | 21 | return { 22 | ...oidc, 23 | getTokens: async () => oidc.getTokens() 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/index.ts: -------------------------------------------------------------------------------- 1 | export type { OnyxiaApi } from "./OnyxiaApi"; 2 | export type { Catalog } from "./Catalog"; 3 | export type { Chart } from "./Chart"; 4 | export type { DeploymentRegion } from "./DeploymentRegion"; 5 | export { type JSONSchema, zJSONSchema } from "./JSONSchema"; 6 | export { 7 | type Language, 8 | type LocalizedString, 9 | languages, 10 | zLanguage, 11 | zLocalizedString 12 | } from "./Language"; 13 | export type { Project } from "./Project"; 14 | export type { HelmRelease } from "./HelmRelease"; 15 | export type { User } from "./User"; 16 | export type { XOnyxiaContext } from "./XOnyxia"; 17 | export type { OidcParams, OidcParams_Partial } from "./OidcParams"; 18 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/mutateHelmValues/mutateHelmValues_removeArrayItem.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { mutateHelmValues_removeArrayItem } from "./mutateHelmValues_removeArrayItem"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | 5 | describe(symToStr({ mutateHelmValues_removeArrayItem }), () => { 6 | it("simple case", () => { 7 | const helmValues = { r: [1, 2, 3] }; 8 | 9 | mutateHelmValues_removeArrayItem({ 10 | helmValues, 11 | helmValuesPath: ["r"], 12 | index: 1 13 | }); 14 | 15 | expect(helmValues).toStrictEqual({ 16 | r: [1, 3] 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerUploadModal/ExplorerUploadModalDropArea.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerUploadModalDropArea } from "./ExplorerUploadModalDropArea"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyFiles/Explorer/ExplorerUploadModal/ExplorerUploadModalDropArea", 7 | component: ExplorerUploadModalDropArea 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | onRequestFilesUpload: action("onRequestFilesUpload") 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/tex.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/tools/smartTrim.ts: -------------------------------------------------------------------------------- 1 | export function smartTrim(params: { 2 | text: string; 3 | maxLength: number; 4 | minCharAtTheEnd: number; 5 | }): string { 6 | const { text, maxLength, minCharAtTheEnd } = params; 7 | 8 | if (!text || maxLength < 1 || text.length <= maxLength) { 9 | return text; 10 | } 11 | 12 | if (maxLength === 1) { 13 | return text.substring(0, 1) + "..."; 14 | } 15 | 16 | const left = text.substr(0, text.length - minCharAtTheEnd); 17 | 18 | const right = text.substr(-minCharAtTheEnd); 19 | 20 | const maxLeft = maxLength - minCharAtTheEnd - 3; 21 | 22 | const croppedLeft = left.substr(0, maxLeft); 23 | 24 | return croppedLeft + "..." + right; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/core/tools/streamToArrayBuffer.ts: -------------------------------------------------------------------------------- 1 | export async function streamToArrayBuffer( 2 | stream: ReadableStream 3 | ): Promise { 4 | const reader = stream.getReader(); 5 | const chunks: Uint8Array[] = []; 6 | 7 | while (true) { 8 | const { done, value } = await reader.read(); 9 | if (done) break; 10 | if (value) chunks.push(value); 11 | } 12 | 13 | const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); 14 | const result = new Uint8Array(totalLength); 15 | 16 | let offset = 0; 17 | for (const chunk of chunks) { 18 | result.set(chunk, offset); 19 | offset += chunk.length; 20 | } 21 | 22 | return result.buffer; 23 | } 24 | -------------------------------------------------------------------------------- /web/src/core/usecases/autoLogoutCountdown/state.ts: -------------------------------------------------------------------------------- 1 | import { id } from "tsafe/id"; 2 | import { createUsecaseActions } from "clean-architecture"; 3 | 4 | export type State = { 5 | secondsLeft: number | undefined; 6 | }; 7 | 8 | export const name = "autoLogoutCountdown"; 9 | 10 | export const { reducer, actions } = createUsecaseActions({ 11 | name, 12 | initialState: id({ 13 | secondsLeft: undefined 14 | }), 15 | reducers: { 16 | secondsLeftSet: ( 17 | state, 18 | { payload }: { payload: { secondsLeft: number | undefined } } 19 | ) => { 20 | const { secondsLeft } = payload; 21 | 22 | state.secondsLeft = secondsLeft; 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/json.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/core/tools/highlightMatches.ts: -------------------------------------------------------------------------------- 1 | import { replaceAll } from "./String.prototype.replaceAll"; 2 | 3 | export function getMatchPositions(params: { text: string; search: string }) { 4 | const { text, search } = params; 5 | 6 | const escapedSearch = search.trim().replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); 7 | const regexp = RegExp("(" + replaceAll(escapedSearch, " ", "|") + ")", "ig"); 8 | let result; 9 | const matchPositions: number[] = []; 10 | 11 | if (text) { 12 | while ((result = regexp.exec(text)) !== null) { 13 | for (let i = result.index; i < regexp.lastIndex; i++) { 14 | matchPositions.push(i); 15 | } 16 | } 17 | } 18 | 19 | return matchPositions; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigManagement/decoupledLogic/projectS3ConfigId.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | 3 | const prefix = "project-"; 4 | 5 | export function getProjectS3ConfigId(params: { creationTime: number }): string { 6 | const { creationTime } = params; 7 | 8 | return `${prefix}${creationTime}`; 9 | } 10 | 11 | export function parseProjectS3ConfigId(params: { s3ConfigId: string }): { 12 | creationTime: number; 13 | } { 14 | const { s3ConfigId } = params; 15 | 16 | const creationTimeStr = s3ConfigId.replace(prefix, ""); 17 | 18 | const creationTime = parseInt(creationTimeStr); 19 | 20 | assert(!isNaN(creationTime), "Not a valid s3 project config id"); 21 | 22 | return { creationTime }; 23 | } 24 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | # bundle analysis 13 | /bundle-report 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | *.zip 26 | 27 | android 28 | # We ignore package-lock.json as we recommend yarn 29 | package-lock.json 30 | .eslintcache 31 | 32 | /dist_keycloak 33 | /.yarn_home 34 | /.husky/_ 35 | 36 | # Jetbrains 37 | .idea/ 38 | .env.local.yaml 39 | /src/pluginSystem.js 40 | 41 | *storybook.log -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/shared/SettingField.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { SettingField } from "./SettingField"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Shared/SettingField", 7 | component: SettingField 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | type: "service password", 17 | groupProjectName: "Project A", 18 | servicePassword: "mypassword", 19 | onRequestServicePasswordRenewal: () => action("Password renewed"), 20 | onRequestCopy: () => action("Copied to clipboard") 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /web/src/core/tools/structuredCloneButFunctions.ts: -------------------------------------------------------------------------------- 1 | import "./Object.fromEntries"; 2 | 3 | /** 4 | * Functionally equivalent to structuredClone but 5 | * functions are not cloned but kept as is. 6 | * (as opposed to structuredClone that chokes if it encounters a function) 7 | */ 8 | export function structuredCloneButFunctions(o: T): T { 9 | if (!(o instanceof Object)) { 10 | return o; 11 | } 12 | 13 | if (typeof o === "function") { 14 | return o; 15 | } 16 | 17 | if (o instanceof Array) { 18 | return o.map(structuredCloneButFunctions) as any; 19 | } 20 | 21 | return Object.fromEntries( 22 | Object.entries(o).map(([key, value]) => [key, structuredCloneButFunctions(value)]) 23 | ) as any; 24 | } 25 | -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/RootFormComponent/FormFieldGroupComponent/AutoInjectSwitch.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { AutoInjectSwitch } from "./AutoInjectSwitch"; 3 | import { useState } from "react"; 4 | 5 | const meta = { 6 | title: "pages/Launcher/AutoInjectSwitch", 7 | component: StoryWrapper 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | function StoryWrapper() { 15 | const [isAutoInjected, setIsAutoInjected] = useState(true); 16 | 17 | return ( 18 | 19 | ); 20 | } 21 | 22 | export const Default: Story = { 23 | args: {} 24 | }; 25 | -------------------------------------------------------------------------------- /web/src/ui/shared/Datagrid/CustomNoRowsOverlay.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "onyxia-ui/Text"; 2 | import { tss } from "tss"; 3 | import { useTranslation } from "ui/i18n"; 4 | 5 | export function CustomNoRowsOverlay() { 6 | const { t } = useTranslation("ExplorerItems"); 7 | 8 | const { classes } = useStyles(); 9 | return ( 10 | 11 | {t("empty directory")} 12 | 13 | ); 14 | } 15 | 16 | const useStyles = tss.withName({ CustomNoRowsOverlay }).create(() => ({ 17 | root: { 18 | width: "100%", 19 | height: "100%", 20 | display: "flex", 21 | alignSelf: "center", 22 | alignItems: "center", 23 | justifyContent: "center" 24 | } 25 | })); 26 | -------------------------------------------------------------------------------- /web/src/ui/shared/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { CopyBlock, atomOneLight, atomOneDark } from "react-code-blocks"; 3 | 4 | export type Props = { 5 | initScript: { 6 | scriptCode: string; 7 | programmingLanguage: string; 8 | }; 9 | isDarkModeEnabled: boolean; 10 | }; 11 | 12 | const CodeBlock = memo((props: Props) => { 13 | const { initScript, isDarkModeEnabled } = props; 14 | 15 | return ( 16 | 23 | ); 24 | }); 25 | 26 | export default CodeBlock; 27 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/login/tools/getReferrerUrl.ts: -------------------------------------------------------------------------------- 1 | function getRefererUrlFromUrl() { 2 | const queryParams = new URLSearchParams(window.location.search); 3 | const redirectUri = queryParams.get("redirect_uri"); 4 | 5 | if (redirectUri === null) { 6 | return undefined; 7 | } 8 | 9 | const redirectUrl = new URL(redirectUri); 10 | 11 | return redirectUrl.origin; 12 | } 13 | 14 | const localStorageKey = "theme-onyxia_refererUrl"; 15 | 16 | export function getReferrerUrl() { 17 | const refererUrl = getRefererUrlFromUrl(); 18 | 19 | if (refererUrl === undefined) { 20 | return localStorage.getItem(localStorageKey) ?? undefined; 21 | } 22 | 23 | localStorage.setItem(localStorageKey, refererUrl); 24 | 25 | return refererUrl; 26 | } 27 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/shared/DirectoryOrFileDetailed.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { DirectoryOrFileDetailed } from "./DirectoryOrFileDetailed"; 3 | 4 | const meta = { 5 | title: "Pages/MyFiles/shared/DirectoryOrFileDetailed", 6 | component: DirectoryOrFileDetailed 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Directory: Story = { 14 | args: { 15 | name: "My Documents", 16 | kind: "directory", 17 | isPublic: false 18 | } 19 | }; 20 | 21 | export const File: Story = { 22 | args: { 23 | name: "report.pdf", 24 | kind: "file", 25 | isPublic: true 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/HomeIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/core/tools/parseUrl.ts: -------------------------------------------------------------------------------- 1 | export function parseUrl(url: string) { 2 | const { host, protocol, pathname } = new URL(url); 3 | return { 4 | host, 5 | port: (() => { 6 | const portStr = pathname.split(":")[1]; 7 | 8 | if (portStr !== undefined) { 9 | const port = parseInt(portStr); 10 | 11 | if (isNaN(port)) { 12 | throw new Error("url malformed"); 13 | } 14 | 15 | return port; 16 | } 17 | 18 | switch (protocol) { 19 | case "http:": 20 | return 80; 21 | case "https:": 22 | return 443; 23 | } 24 | 25 | return undefined; 26 | })() 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/SecretsExplorer/SecretsExplorerButtonBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { SecretsExplorerButtonBar } from "./SecretsExplorerButtonBar"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MySecrets/SecretsExplorer/SecretsExplorerButtonBar", 7 | component: SecretsExplorerButtonBar 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | selectedItemKind: "none", // No file or directory selected 17 | isSelectedItemInEditingState: false, 18 | isFileOpen: false, 19 | callback: action("Button clicked") 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/CodeTextEditor/JsonCodeTextEditor.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { json } from "@codemirror/lang-json"; 3 | import { assert, type Equals } from "tsafe/assert"; 4 | import { TextEditor } from "../TextEditor"; 5 | 6 | export type Props = { 7 | className?: string; 8 | id?: string; 9 | maxHeight?: number; 10 | value: string; 11 | onChange: ((newValue: string) => void) | undefined; 12 | fallback?: JSX.Element; 13 | children?: ReactNode; 14 | }; 15 | 16 | { 17 | type Props_Expected = Omit; 18 | 19 | assert>; 20 | } 21 | 22 | export default function JsonCodeTextEditor(props: Props) { 23 | return ; 24 | } 25 | -------------------------------------------------------------------------------- /helm-chart/templates/api/configmaps-schemas.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.api.schemas.enabled }} 2 | {{- $fullname := include "onyxia.fullname" . -}} 3 | {{- range .Values.api.schemas.files }} 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: {{ $fullname }}-{{ printf "%s %s" .relativePath "default" | sha256sum | trunc 8 }} 8 | data: 9 | {{ .relativePath | base }}: |- 10 | {{ .content | indent 4 }} 11 | --- 12 | {{- end }} 13 | {{- range .Values.api.schemas.roles }} 14 | {{- $role := .roleName -}} 15 | {{- range .files }} 16 | apiVersion: v1 17 | kind: ConfigMap 18 | metadata: 19 | name: {{ $fullname }}-{{ printf "%s %s %s" .relativePath "role" $role | sha256sum | trunc 8 }} 20 | data: 21 | {{ .relativePath | base }}: |- 22 | {{ .content | indent 4 }} 23 | --- 24 | {{- end }} 25 | {{- end }} 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/User.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/secret.svg: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /web/src/ui/tools/useOnOpenBrowserSearch.ts: -------------------------------------------------------------------------------- 1 | import { useConstCallback } from "powerhooks/useConstCallback"; 2 | import { useEffect } from "react"; 3 | 4 | export function useOnOpenBrowserSearch(onOpenSearch: () => void) { 5 | const onOpenSearchConst = useConstCallback(onOpenSearch); 6 | 7 | useEffect(() => { 8 | const handleKeyPress = (event: KeyboardEvent) => { 9 | // Check for Ctrl + F on Windows/Linux or Command + F on macOS 10 | if ((event.ctrlKey || event.metaKey) && event.key === "f") { 11 | onOpenSearchConst(); 12 | } 13 | }; 14 | 15 | window.addEventListener("keydown", handleKeyPress); 16 | 17 | return () => { 18 | window.removeEventListener("keydown", handleKeyPress); 19 | }; 20 | }, []); 21 | } 22 | -------------------------------------------------------------------------------- /web/src/core/tools/getValueAtPathInObject.ts: -------------------------------------------------------------------------------- 1 | /** NOTE: It returns undefined if the path is not found */ 2 | export function getValueAtPathInObject(params: { 3 | path: (string | number)[]; 4 | obj: any; 5 | }): T | undefined { 6 | return getValueAtPathInObjectRec(params); 7 | } 8 | 9 | function getValueAtPathInObjectRec(params: { 10 | path: (string | number)[]; 11 | obj: any; 12 | }): T | undefined { 13 | const { path, obj } = params; 14 | 15 | if (path.length === 0) { 16 | return obj; 17 | } 18 | 19 | if (!(obj instanceof Object)) { 20 | return undefined; 21 | } 22 | 23 | const [key, ...newPath] = path; 24 | 25 | const newObj = obj[key]; 26 | 27 | return getValueAtPathInObjectRec({ 28 | path: newPath, 29 | obj: newObj 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /web/src/core/usecases/launcher/decoupledLogic/mutateHelmValues/mutateHelmValues_removeArrayItem.ts: -------------------------------------------------------------------------------- 1 | import type { Stringifyable } from "core/tools/Stringifyable"; 2 | import { getValueAtPath } from "core/tools/Stringifyable"; 3 | import { assert } from "tsafe/assert"; 4 | 5 | export function mutateHelmValues_removeArrayItem(params: { 6 | helmValues: Record; 7 | helmValuesPath: (string | number)[]; 8 | index: number; 9 | }): void { 10 | const { helmValues, helmValuesPath, index } = params; 11 | 12 | const arr = getValueAtPath({ 13 | stringifyableObjectOrArray: helmValues, 14 | path: helmValuesPath, 15 | doDeleteFromSource: false, 16 | doFailOnUnresolved: false 17 | }); 18 | 19 | assert(arr instanceof Array); 20 | 21 | arr.splice(index, 1); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/dispatch_on_new_release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | 5 | jobs: 6 | 7 | notify_paris_sspcloud: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 11 | with: 12 | repository: InseeFrLab/paris-sspcloud 13 | event-type: onyxia_release 14 | client-payload: '{"release_tag_name": "${{github.event.release.tag_name}}"}' 15 | token: ${{secrets.MY_GITHUB_TOKEN}} 16 | - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 17 | with: 18 | repository: InseeFrLab/onyxia-ops 19 | event-type: onyxia_release 20 | client-payload: '{"release_tag_name": "${{github.event.release.tag_name}}"}' 21 | token: ${{secrets.MY_GITHUB_TOKEN}} -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerIcon/ExplorerIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerIcon } from "./ExplorerIcon"; 3 | 4 | const meta = { 5 | title: "Pages/MyFiles/Explorer/ExplorerIcon", 6 | component: ExplorerIcon 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const DataIconWithShadow: Story = { 14 | args: { 15 | iconId: "data", 16 | hasShadow: true 17 | } 18 | }; 19 | 20 | export const DirectoryIconWithoutShadow: Story = { 21 | args: { 22 | iconId: "directory", 23 | hasShadow: false 24 | } 25 | }; 26 | 27 | export const JsonIcon: Story = { 28 | args: { 29 | iconId: "json", 30 | hasShadow: false 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /web/src/core/adapters/s3Client/utils/bucketNameAndObjectNameFromS3Path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * "/bucket-name/object/name" => { bucketName: "bucket-name", objectName: "object/name" } 3 | * "bucket-name/object/name" => { bucketName: "bucket-name", objectName: "object/name" } 4 | * "bucket-name/object/name/" => { bucketName: "bucket-name", objectName: "object/name/" } 5 | * "bucket-name/" => { bucketName: "bucket-name", objectName: "" } 6 | * "bucket-name" => { bucketName: "bucket-name", objectName: "" } 7 | * "s3://bucket-name/object/name" => { bucketName: "bucket-name", objectName: "object/name" } 8 | */ 9 | export function bucketNameAndObjectNameFromS3Path(path: string) { 10 | const [bucketName, ...rest] = path.replace(/^(s3:)?\/+/, "").split("/"); 11 | 12 | return { 13 | bucketName, 14 | objectName: rest.join("/") 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/python.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helm-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: onyxia 3 | description: Onyxia is your datalab's hub. 4 | icon: https://inseefrlab.github.io/onyxia/icon.svg 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | version: 10.30.13 18 | -------------------------------------------------------------------------------- /web/src/core/usecases/restorableConfigManagement/decoupledLogic/getAreSameRestorableConfigRef.ts: -------------------------------------------------------------------------------- 1 | import { assert, type Equals } from "tsafe/assert"; 2 | 3 | export type RestorableServiceConfigRef = { 4 | friendlyName: string; 5 | catalogId: string; 6 | chartName: string; 7 | }; 8 | 9 | export function getAreSameRestorableConfigRef( 10 | a: RestorableServiceConfigRef, 11 | b: RestorableServiceConfigRef 12 | ) { 13 | const keys = ["friendlyName", "catalogId", "chartName"] as const; 14 | 15 | assert>; 16 | 17 | for (const key of keys) { 18 | const v_a = a[key]; 19 | 20 | assert>(); 21 | 22 | const v_b = b[key]; 23 | 24 | if (v_a !== v_b) { 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /web/src/ui/tools/mergeDeep.ts: -------------------------------------------------------------------------------- 1 | function isObject(item: any) { 2 | return item && typeof item === "object" && !Array.isArray(item); 3 | } 4 | 5 | export function mergeDeep< 6 | A extends Record, 7 | B extends Record 8 | >(a: A, b: B): A & B { 9 | let output = Object.assign({}, a); 10 | if (isObject(a) && isObject(b)) { 11 | Object.keys(b).forEach(key => { 12 | if (isObject(b[key])) { 13 | if (!(key in a)) Object.assign(output, { [key]: b[key] }); 14 | else { 15 | // @ts-expect-error 16 | output[key] = mergeDeep(a[key], b[key]); 17 | } 18 | } else { 19 | Object.assign(output, { [key]: b[key] }); 20 | } 21 | }); 22 | } 23 | // @ts-expect-error 24 | return output; 25 | } 26 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2022.Error"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "baseUrl": "src", 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "verbatimModuleSyntax": true 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/MyServicesRestorableConfigs/MyServicesRestorableConfig/MyServicesRestorableConfigOptions.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { MyServicesRestorableConfigOptions } from "./MyServicesRestorableConfigOptions"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyServices/MyServicesRestorableConfigs/MyServicesRestorableConfig/MyServicesRestorableConfigOptions", 7 | component: MyServicesRestorableConfigOptions 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | callback: action("callback"), 17 | doDisableMoveDown: false, 18 | doDisableMoveUp: false, 19 | isShortVariant: false 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/CodeTextEditor/ShellCodeTextEditor.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { shell } from "@codemirror/legacy-modes/mode/shell"; 3 | import { StreamLanguage } from "@codemirror/language"; 4 | import { assert, type Equals } from "tsafe/assert"; 5 | import { TextEditor } from "../TextEditor"; 6 | 7 | export type Props = { 8 | className?: string; 9 | id?: string; 10 | maxHeight?: number; 11 | value: string; 12 | onChange: ((newValue: string) => void) | undefined; 13 | fallback?: JSX.Element; 14 | children?: ReactNode; 15 | }; 16 | 17 | { 18 | type Props_Expected = Omit; 19 | 20 | assert>; 21 | } 22 | 23 | export default function ShellCodeTextEditor(props: Props) { 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/ui/pages/dataExplorer/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, createGroup } from "type-route"; 2 | import { id } from "tsafe/id"; 3 | import type { ValueSerializer } from "type-route"; 4 | 5 | export const routeDefs = { 6 | dataExplorer: defineRoute( 7 | { 8 | source: param.query.optional.string, 9 | rowsPerPage: param.query.optional.number, 10 | page: param.query.optional.number, 11 | selectedRow: param.query.optional.string, 12 | columnVisibility: param.query.optional.ofType( 13 | id>>({ 14 | parse: raw => JSON.parse(raw), 15 | stringify: value => JSON.stringify(value) 16 | }) 17 | ) 18 | }, 19 | () => `/data-explorer` 20 | ) 21 | }; 22 | 23 | export const routeGroup = createGroup(routeDefs); 24 | -------------------------------------------------------------------------------- /web/src/core/usecases/serviceDetails/evt.ts: -------------------------------------------------------------------------------- 1 | import type { CreateEvt } from "core/bootstrap"; 2 | import { name } from "./state"; 3 | import { Evt } from "evt"; 4 | 5 | export const createEvt = (({ evtAction }) => { 6 | const evtOut = Evt.create<{ 7 | actionName: "redirect away"; 8 | }>(); 9 | 10 | evtAction.attach( 11 | action => 12 | action.usecaseName === "projectManagement" && 13 | action.actionName === "projectChanged", 14 | () => { 15 | evtOut.post({ actionName: "redirect away" }); 16 | } 17 | ); 18 | 19 | evtAction.attach( 20 | action => 21 | action.usecaseName === name && 22 | action.actionName === "notifyHelmReleaseNoLongerExists", 23 | () => { 24 | evtOut.post({ actionName: "redirect away" }); 25 | } 26 | ); 27 | 28 | return evtOut; 29 | }) satisfies CreateEvt; 30 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/sass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/RootFormComponent/FormCallbacks.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FormField, 3 | FormFieldGroup, 4 | FormFieldValue 5 | } from "core/usecases/launcher/decoupledLogic/formTypes"; 6 | 7 | export type FormCallbacks = { 8 | onChange: (params: { 9 | formFieldValue: FormFieldValue; 10 | isAutocompleteOptionSelection: boolean; 11 | }) => void; 12 | onAdd: (params: { helmValuesPath: (string | number)[] }) => void; 13 | onRemove: (params: { helmValuesPath: (string | number)[]; index: number }) => void; 14 | onFieldErrorChange: (params: { 15 | helmValuesPath: (string | number)[]; 16 | hasError: boolean; 17 | }) => void; 18 | onIsAutoInjectedChange: (params: { 19 | helmValuesPath: (string | number)[]; 20 | isAutoInjected: boolean; 21 | }) => void; 22 | onAutocompletePanelOpen: (params: { helmValuesPath: (string | number)[] }) => void; 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/core/usecases/deploymentRegionManagement/selectors.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import type { DeploymentRegion } from "core/ports/OnyxiaApi"; 3 | import { name } from "./state"; 4 | import type { State as RootState } from "core/bootstrap"; 5 | 6 | const currentDeploymentRegion = (rootState: RootState): DeploymentRegion => { 7 | const { currentDeploymentRegionId, availableDeploymentRegions } = rootState[name]; 8 | 9 | const selectedDeploymentRegion = availableDeploymentRegions.find( 10 | ({ id }) => id === currentDeploymentRegionId 11 | ); 12 | 13 | assert(selectedDeploymentRegion !== undefined); 14 | 15 | return selectedDeploymentRegion; 16 | }; 17 | 18 | const availableDeploymentRegionIds = (rootState: RootState): string[] => 19 | rootState[name].availableDeploymentRegions.map(({ id }) => id); 20 | 21 | export const selectors = { currentDeploymentRegion, availableDeploymentRegionIds }; 22 | -------------------------------------------------------------------------------- /web/src/core/usecases/userAuthentication/state.ts: -------------------------------------------------------------------------------- 1 | import type { User } from "core/ports/OnyxiaApi"; 2 | import { 3 | createUsecaseActions, 4 | createObjectThatThrowsIfAccessed 5 | } from "clean-architecture"; 6 | 7 | export const name = "userAuthentication"; 8 | 9 | // Just so it can be accessed in other selectors. 10 | type State = State.NotLoggedIn | State.LoggedIn; 11 | 12 | namespace State { 13 | export type NotLoggedIn = { 14 | isUserLoggedIn: false; 15 | }; 16 | export type LoggedIn = { 17 | isUserLoggedIn: true; 18 | user: User; 19 | isKeycloak: boolean; 20 | }; 21 | } 22 | 23 | export const { reducer, actions } = createUsecaseActions({ 24 | name, 25 | initialState: createObjectThatThrowsIfAccessed({ 26 | debugMessage: "Not initialized yet" 27 | }), 28 | reducers: { 29 | initialized: (_, { payload }: { payload: State }) => payload 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /web/src/ui/tools/getRefFromDeps.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from "react"; 2 | import memoize from "memoizee"; 3 | 4 | export function getRefFromDepsFactory(params: { max: number }) { 5 | const { max } = params; 6 | 7 | const memoizedByNumberOfArgument = new Map< 8 | number, 9 | (...deps: DependencyList) => object 10 | >(); 11 | 12 | function getRefFromDeps(deps: DependencyList): object { 13 | let memoized = memoizedByNumberOfArgument.get(deps.length); 14 | 15 | if (memoized !== undefined) { 16 | return memoized(...deps); 17 | } 18 | 19 | memoizedByNumberOfArgument.set( 20 | deps.length, 21 | memoize((..._deps: DependencyList) => ({}), { 22 | length: deps.length, 23 | max 24 | }) 25 | ); 26 | 27 | return getRefFromDeps(deps); 28 | } 29 | 30 | return { getRefFromDeps }; 31 | } 32 | -------------------------------------------------------------------------------- /web/src/core/usecases/restorableConfigManagement/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from "clean-architecture"; 2 | import type { State as RootState } from "core/bootstrap"; 3 | import { name } from "./state"; 4 | import * as projectManagement from "core/usecases/projectManagement"; 5 | 6 | function state(rootState: RootState) { 7 | return rootState[name]; 8 | } 9 | 10 | const restorableConfigs = createSelector( 11 | projectManagement.protectedSelectors.projectConfig, 12 | createSelector(state, state => state.indexedChartsIcons), 13 | ({ restorableConfigs }, indexedChartsIcons) => 14 | restorableConfigs.map(restorableConfig => ({ 15 | ...restorableConfig, 16 | chartIconUrl: 17 | indexedChartsIcons[restorableConfig.catalogId]?.[ 18 | restorableConfig.chartName 19 | ] 20 | })) 21 | ); 22 | 23 | export const selectors = { 24 | restorableConfigs 25 | }; 26 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/audio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, noMatch, createGroup } from "type-route"; 2 | import type { ValueSerializer } from "type-route"; 3 | import { id } from "tsafe/id"; 4 | import { tabIds, type TabId } from "./tabIds"; 5 | 6 | export const routeDefs = { 7 | projectSettings: defineRoute( 8 | { 9 | tabId: param.path.optional 10 | .ofType( 11 | id>({ 12 | parse: raw => 13 | !id(tabIds).includes(raw) 14 | ? noMatch 15 | : (raw as TabId), 16 | stringify: value => value 17 | }) 18 | ) 19 | .default(tabIds[0]) 20 | }, 21 | ({ tabId }) => `/project-settings/${tabId}` 22 | ) 23 | }; 24 | 25 | export const routeGroup = createGroup(routeDefs); 26 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/main.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import { StrictMode, lazy, Suspense } from "react"; 3 | import { createRoot } from "react-dom/client"; 4 | import { assert } from "tsafe/assert"; 5 | import { OnyxiaUi, loadThemedFavicon } from "keycloak-theme/login/theme"; 6 | import { injectCustomFontFaceIfNotAlreadyDone } from "ui/theme/injectCustomFontFaceIfNotAlreadyDone"; 7 | 8 | injectCustomFontFaceIfNotAlreadyDone(); 9 | loadThemedFavicon(); 10 | 11 | assert(window.kcContext !== undefined); 12 | 13 | const KcLoginThemePage = lazy(() => import("keycloak-theme/login/KcPage")); 14 | 15 | createRoot(document.getElementById("root")!).render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /web/src/ui/pages/page404/Page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "ui/i18n"; 2 | import { tss } from "tss"; 3 | import { Text } from "onyxia-ui/Text"; 4 | import { declareComponentKeys } from "i18nifty"; 5 | 6 | const Page = Page404; 7 | export default Page; 8 | 9 | function Page404() { 10 | const { t } = useTranslation({ Page404 }); 11 | 12 | const { classes } = useStyles(); 13 | 14 | return ( 15 |
16 | {t("not found")} 😥 17 |
18 | ); 19 | } 20 | 21 | const useStyles = tss.withName({ Page404 }).create(({ theme }) => ({ 22 | root: { 23 | height: "100%", 24 | display: "flex", 25 | alignItems: "center", 26 | justifyContent: "center", 27 | backgroundColor: theme.colors.useCases.surfaces.background 28 | } 29 | })); 30 | 31 | const { i18n } = declareComponentKeys<"not found">()({ Page404 }); 32 | export type I18n = typeof i18n; 33 | -------------------------------------------------------------------------------- /web/public/custom-resources/my-plugin.js: -------------------------------------------------------------------------------- 1 | 2 | console.log("Initializing Onyxia plugin at /custom-resources/my-plugin.js"); 3 | 4 | window.addEventListener("onyxiaready", ()=> { 5 | 6 | const onyxia = window.onyxia; 7 | 8 | onyxia.addEventListener(eventName=> { 9 | switch(eventName){ 10 | case "theme updated": 11 | console.log("Onyxia theme updated: ", onyxia.theme); 12 | break; 13 | case "language changed": 14 | console.log(`Language changed to ${onyxia.lang}`); 15 | break; 16 | case "route changed": 17 | console.log(`Route changed: ${onyxia.route.name}`); 18 | break; 19 | case "route params changed": 20 | console.log(`Route params changed: `, onyxia.route.params); 21 | break; 22 | default: 23 | } 24 | 25 | }); 26 | 27 | console.log("Onyxia Global API ready", onyxia); 28 | 29 | }); -------------------------------------------------------------------------------- /web/src/core/tools/timeAgo.ts: -------------------------------------------------------------------------------- 1 | export function timeAgo(unixTimestampMs: number): string { 2 | const msPerSecond = 1000; 3 | const msPerMinute = 60 * 1000; 4 | const msPerHour = 60 * 60 * 1000; 5 | const msPerDay = 24 * 60 * 60 * 1000; 6 | 7 | const now = new Date().getTime(); 8 | const elapsed = now - unixTimestampMs; 9 | 10 | if (elapsed < msPerMinute) { 11 | const seconds = Math.round(elapsed / msPerSecond); 12 | return `${seconds} second${seconds > 1 ? "s" : ""} ago`; 13 | } else if (elapsed < msPerHour) { 14 | const minutes = Math.round(elapsed / msPerMinute); 15 | return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; 16 | } else if (elapsed < msPerDay) { 17 | const hours = Math.round(elapsed / msPerHour); 18 | return `${hours} hour${hours > 1 ? "s" : ""} ago`; 19 | } else { 20 | const days = Math.round(elapsed / msPerDay); 21 | return `${days} day${days > 1 ? "s" : ""} ago`; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/src/ui/pages/account/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, noMatch, createGroup } from "type-route"; 2 | import type { ValueSerializer } from "type-route"; 3 | import { id } from "tsafe/id"; 4 | import { accountTabIds, type AccountTabId } from "./accountTabIds"; 5 | 6 | export const routeDefs = { 7 | account: defineRoute( 8 | { 9 | tabId: param.path.optional 10 | .ofType( 11 | id>({ 12 | parse: raw => 13 | !id(accountTabIds).includes(raw) 14 | ? noMatch 15 | : (raw as AccountTabId), 16 | stringify: value => value 17 | }) 18 | ) 19 | .default(accountTabIds[0]) 20 | }, 21 | ({ tabId }) => `/account/${tabId}` 22 | ) 23 | }; 24 | 25 | export const routeGroup = createGroup(routeDefs); 26 | -------------------------------------------------------------------------------- /web/src/keycloak-theme/login/theme.ts: -------------------------------------------------------------------------------- 1 | import { createOnyxiaUi, defaultGetTypographyDesc } from "onyxia-ui"; 2 | import { getPalette } from "ui/theme/palette"; 3 | import { targetWindowInnerWidth } from "ui/theme/targetWindowInnerWidth"; 4 | import { env } from "env"; 5 | import { loadThemedFavicon as loadThemedFavicon_base } from "ui/theme/loadThemedFavicon"; 6 | import { Evt } from "evt"; 7 | 8 | const { OnyxiaUi, evtTheme } = createOnyxiaUi({ 9 | getTypographyDesc: params => ({ 10 | ...defaultGetTypographyDesc({ 11 | ...params, 12 | // NOTE: Prevent the font from being responsive. 13 | windowInnerWidth: targetWindowInnerWidth 14 | }), 15 | fontFamily: `'${env.FONT.fontFamily}', 'Roboto', sans-serif` 16 | }), 17 | palette: getPalette, 18 | splashScreenParams: undefined 19 | }); 20 | 21 | export { OnyxiaUi }; 22 | 23 | export const loadThemedFavicon = () => 24 | loadThemedFavicon_base({ 25 | evtTheme: Evt.loosenType(evtTheme) 26 | }); 27 | -------------------------------------------------------------------------------- /web/src/ui/shared/CopyToClipboardIconButton.tsx: -------------------------------------------------------------------------------- 1 | import { CopyToClipboardIconButton as CopyToClipboardIconButtonNoTranslations } from "onyxia-ui/CopyToClipboardIconButton"; 2 | import { declareComponentKeys } from "i18nifty"; 3 | import { useTranslation } from "ui/i18n"; 4 | 5 | type Props = { 6 | className?: string; 7 | textToCopy: string; 8 | }; 9 | 10 | export function CopyToClipboardIconButton(props: Props) { 11 | const { className, textToCopy } = props; 12 | 13 | const { t } = useTranslation({ CopyToClipboardIconButton }); 14 | 15 | return ( 16 | 22 | ); 23 | } 24 | 25 | const { i18n } = declareComponentKeys<"copy to clipboard" | "copied to clipboard">()({ 26 | CopyToClipboardIconButton 27 | }); 28 | export type I18n = typeof i18n; 29 | -------------------------------------------------------------------------------- /web/src/core/usecases/catalog/evt.ts: -------------------------------------------------------------------------------- 1 | import "minimal-polyfills/Object.fromEntries"; 2 | import type { CreateEvt } from "core/bootstrap"; 3 | import { Evt } from "evt"; 4 | import { name } from "./state"; 5 | import { assert } from "tsafe/assert"; 6 | 7 | export const createEvt = (({ evtAction, getState }) => { 8 | const evtOut = Evt.create<{ 9 | actionName: "catalogIdInternallySet"; 10 | catalogId: string; 11 | }>(); 12 | 13 | evtAction 14 | .pipe(action => (action.usecaseName !== name ? null : [action])) 15 | .attach( 16 | ({ actionName }) => actionName === "defaultCatalogSelected", 17 | () => { 18 | const state = getState()[name]; 19 | assert(state.stateDescription === "ready"); 20 | evtOut.post({ 21 | actionName: "catalogIdInternallySet", 22 | catalogId: state.selectedCatalogId 23 | }); 24 | } 25 | ); 26 | 27 | return evtOut; 28 | }) satisfies CreateEvt; 29 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/KeyFile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/ui/pages/mySecrets/MySecretsEditor/MySecretsEditorRow.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { MySecretsEditorRow } from "./MySecretsEditorRow"; 3 | import { Evt } from "evt"; 4 | import { action } from "@storybook/addon-actions"; 5 | 6 | const meta = { 7 | title: "Pages/MySecrets/MySecretsEditor/MySecretsEditorRow", 8 | component: MySecretsEditorRow 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = { 16 | args: { 17 | isLocked: false, 18 | keyOfSecret: "API_KEY", 19 | strValue: "12345", 20 | onEdit: action("Edit secret"), 21 | onDelete: action("Delete secret"), 22 | getIsValidAndAvailableKey: () => ({ 23 | isValidAndAvailableKey: true 24 | }), 25 | onStartEdit: action("Start editing"), 26 | evtAction: Evt.create<"ENTER EDITING STATE" | "SUBMIT EDIT">(), 27 | isDarker: false 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /web/.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming"; 2 | 3 | export const darkTheme = create({ 4 | base: "dark", 5 | appBg: "#2c323f", 6 | appContentBg: "#2c323f", 7 | appPreviewBg: "#2c323f", 8 | barBg: "#2c323f", 9 | colorSecondary: "#ff562c", 10 | textColor: "#f1f0eb", 11 | brandImage: "onyxiaLogo.png", 12 | brandTitle: "Onyxia UI", 13 | brandUrl: "https://github.com/InseeFrLab/onyxia-ui", 14 | fontBase: '"Work Sans","Open Sans", sans-serif', 15 | fontCode: "monospace" 16 | }); 17 | 18 | export const lightTheme = create({ 19 | base: "light", 20 | appBg: "#f1f0eb", 21 | appContentBg: "#f1f0eb", 22 | appPreviewBg: "#f1f0eb", 23 | barBg: "#f1f0eb", 24 | colorSecondary: "#ff562c", 25 | textColor: "#2c323f", 26 | textInverseColor: "#f1f0eb", 27 | brandImage: "onyxiaLogo.png", 28 | brandTitle: "Onyxia UI", 29 | brandUrl: "https://github.com/InseeFrLab/onyxia-ui", 30 | fontBase: '"Work Sans","Open Sans", sans-serif', 31 | fontCode: "monospace" 32 | }); 33 | -------------------------------------------------------------------------------- /web/src/core/usecases/s3ConfigManagement/state.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedAdminBookmark } from "./decoupledLogic/resolveS3AdminBookmarks"; 2 | import { 3 | createUsecaseActions, 4 | createObjectThatThrowsIfAccessed 5 | } from "clean-architecture"; 6 | 7 | type State = { 8 | resolvedAdminBookmarks: ResolvedAdminBookmark[]; 9 | }; 10 | 11 | export const name = "s3ConfigManagement"; 12 | 13 | export const { reducer, actions } = createUsecaseActions({ 14 | name, 15 | initialState: createObjectThatThrowsIfAccessed(), 16 | reducers: { 17 | initialized: ( 18 | _, 19 | { 20 | payload 21 | }: { 22 | payload: { 23 | resolvedAdminBookmarks: ResolvedAdminBookmark[]; 24 | }; 25 | } 26 | ) => { 27 | const { resolvedAdminBookmarks } = payload; 28 | 29 | const state: State = { 30 | resolvedAdminBookmarks 31 | }; 32 | 33 | return state; 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /web/src/core/usecases/restorableConfigManagement/state.ts: -------------------------------------------------------------------------------- 1 | import { createUsecaseActions } from "clean-architecture"; 2 | import { id } from "tsafe/id"; 3 | 4 | export type State = { 5 | indexedChartsIcons: { 6 | [catalogId: string]: 7 | | { 8 | [chartName: string]: string | undefined; 9 | } 10 | | undefined; 11 | }; 12 | }; 13 | 14 | export const name = "restorableConfigManagement"; 15 | 16 | export const { reducer, actions } = createUsecaseActions({ 17 | name, 18 | initialState: id({ 19 | indexedChartsIcons: {} 20 | }), 21 | reducers: { 22 | initialized: ( 23 | state, 24 | { 25 | payload 26 | }: { 27 | payload: { 28 | indexedChartsIcons: State["indexedChartsIcons"]; 29 | }; 30 | } 31 | ) => { 32 | const { indexedChartsIcons } = payload; 33 | 34 | state.indexedChartsIcons = indexedChartsIcons; 35 | } 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /web/src/lazy-icons.ts: -------------------------------------------------------------------------------- 1 | import { createGetIconUrl } from "mui-icons-material-lazy"; 2 | import { getOnyxiaInstancePublicUrl } from "keycloak-theme/login/onyxiaInstancePublicUrl"; 3 | import { env } from "env"; 4 | 5 | export const { getIconUrl, getIconUrlByName } = createGetIconUrl({ 6 | BASE_URL: (() => { 7 | // NOTE: We delete the icons in the Keycloak theme not increasing the payload too much 8 | // so we fetch the icons from the onyxia instance if needed. 9 | if (window.kcContext !== undefined) { 10 | return getOnyxiaInstancePublicUrl(); 11 | } 12 | 13 | return env.PUBLIC_URL; 14 | })() 15 | }); 16 | 17 | export const customIcons = { 18 | servicesSvgUrl: `${env.PUBLIC_URL}/icons/services.svg?v=2`, 19 | secretsSvgUrl: `${env.PUBLIC_URL}/icons/secrets.svg?v=2`, 20 | accountSvgUrl: `${env.PUBLIC_URL}/icons/account.svg?v=2`, 21 | homeSvgUrl: `${env.PUBLIC_URL}/icons/home.svg?v=2`, 22 | filesSvgUrl: `${env.PUBLIC_URL}/icons/files.svg?v=2`, 23 | catalogSvgUrl: `${env.PUBLIC_URL}/icons/catalog.svg?v=2` 24 | }; 25 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/xls.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/myServices/Quotas/CircularUsage.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CircularUsage } from "./CircularUsage"; 3 | 4 | const meta = { 5 | title: "Pages/MyServices/Quotas/CircularUsage", 6 | component: CircularUsage, 7 | args: { 8 | name: "limits.cpu", 9 | used: "500m", 10 | total: "1000m", 11 | usagePercentage: 50, 12 | severity: "warning" 13 | } 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | 18 | type Story = StoryObj; 19 | 20 | export const Default: Story = {}; 21 | 22 | export const LowUsage: Story = { 23 | args: { 24 | name: "limits.memory", 25 | used: "100Mi", 26 | total: "200Mi", 27 | usagePercentage: 10, 28 | severity: "success" 29 | } 30 | }; 31 | 32 | export const HighUsage: Story = { 33 | args: { 34 | name: "requests.memory", 35 | used: "900Mi", 36 | total: "1000Mi", 37 | usagePercentage: 90, 38 | severity: "error" 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /web/src/ui/tools/generateUniqDefaultName.ts: -------------------------------------------------------------------------------- 1 | function generateUniqDefaultNameRec( 2 | params: Parameters[0] & { 3 | n: number; 4 | } 5 | ): string { 6 | const { names, buildName, n } = params; 7 | 8 | const candidateName = buildName(n); 9 | 10 | if (!names.includes(candidateName)) { 11 | return candidateName; 12 | } 13 | 14 | return generateUniqDefaultNameRec({ 15 | names, 16 | buildName, 17 | n: n + 1 18 | }); 19 | } 20 | 21 | export function generateUniqDefaultName(params: { 22 | names: string[]; 23 | buildName: (n: number) => string; 24 | }): string { 25 | return generateUniqDefaultNameRec({ 26 | ...params, 27 | n: 1 28 | }); 29 | } 30 | 31 | /** buildNameFactory({ "defaultName": "foo", "separator": " " })(33) === "foo 33" */ 32 | export function buildNameFactory(params: { defaultName: string; separator: string }) { 33 | const { defaultName, separator } = params; 34 | 35 | return (n: number) => `${defaultName}${n === 1 ? "" : `${separator}${n - 1}`}`; 36 | } 37 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/DataTextEditor/JsonSchemaDialog.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useId } from "react"; 2 | import { Dialog } from "onyxia-ui/Dialog"; 3 | import { Button } from "onyxia-ui/Button"; 4 | import { CodeTextEditor } from "../CodeTextEditor"; 5 | 6 | export type Props = { 7 | isOpen: boolean; 8 | onClose: () => void; 9 | jsonSchemaStr: string; 10 | }; 11 | 12 | export const JsonSchemaDialog = memo((props: Props) => { 13 | const { isOpen, onClose, jsonSchemaStr } = props; 14 | 15 | const id_editor = useId(); 16 | 17 | return ( 18 | 31 | } 32 | buttons={} 33 | /> 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/ProjectSettingsS3ConfigTab/S3ConfigDialogs/S3ConfigDialogs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ConfirmCustomS3ConfigDeletionDialog, 3 | type Props as ConfirmCustomS3ConfigDeletionDialogProps 4 | } from "./ConfirmCustomS3ConfigDeletionDialog"; 5 | import { 6 | AddCustomS3ConfigDialog, 7 | type AddCustomS3ConfigDialogProps 8 | } from "./AddCustomS3ConfigDialog"; 9 | 10 | export type S3ConfigDialogsProps = { 11 | evtConfirmCustomS3ConfigDeletionDialogOpen: ConfirmCustomS3ConfigDeletionDialogProps["evtOpen"]; 12 | evtAddCustomS3ConfigDialogOpen: AddCustomS3ConfigDialogProps["evtOpen"]; 13 | }; 14 | 15 | export function S3ConfigDialogs(props: S3ConfigDialogsProps) { 16 | const { evtConfirmCustomS3ConfigDeletionDialogOpen, evtAddCustomS3ConfigDialogOpen } = 17 | props; 18 | 19 | return ( 20 | <> 21 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /web/src/ui/tools/useBackgroundColor.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export function useBackgroundColor() { 4 | const TRANSPARENT = "rgba(0, 0, 0, 0)"; 5 | 6 | const [backgroundColor, setBackgroundColor] = useState(TRANSPARENT); 7 | 8 | const [element, setElement] = useState(null); 9 | 10 | useEffect(() => { 11 | if (element === null) { 12 | return; 13 | } 14 | 15 | const backgroundColor = (function callee(element: HTMLElement) { 16 | const { backgroundColor } = window.getComputedStyle(element); 17 | 18 | if (backgroundColor === TRANSPARENT) { 19 | const parent = element.parentElement; 20 | 21 | if (parent === null) { 22 | return backgroundColor; 23 | } 24 | 25 | return callee(parent); 26 | } 27 | 28 | return backgroundColor; 29 | })(element); 30 | 31 | setBackgroundColor(backgroundColor); 32 | }, [element]); 33 | 34 | return { backgroundColor, setElement }; 35 | } 36 | -------------------------------------------------------------------------------- /web/src/ui/i18n/z.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { assert, type Equals } from "tsafe/assert"; 3 | import type { Language } from "core"; 4 | import type { LocalizedString } from "./i18n"; 5 | import { id } from "tsafe/id"; 6 | 7 | //List the languages you with to support 8 | export const languages = [ 9 | "en", 10 | "fr", 11 | "zh-CN", 12 | "no", 13 | "fi", 14 | "nl", 15 | "it", 16 | "es", 17 | "de" 18 | ] as const; 19 | 20 | assert>(); 21 | 22 | export const zLanguage = (() => { 23 | type TargetType = Language; 24 | 25 | const zTargetType = z.enum(languages); 26 | 27 | assert, TargetType>>(); 28 | 29 | return id>(zTargetType); 30 | })(); 31 | 32 | export const zLocalizedString = (() => { 33 | type TargetType = LocalizedString; 34 | 35 | const zTargetType = z.union([z.string(), z.record(zLanguage, z.string())]); 36 | 37 | assert, TargetType>>(); 38 | 39 | return id>(zTargetType); 40 | })(); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/config.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/shared/formattedDate/type.ts: -------------------------------------------------------------------------------- 1 | import { declareComponentKeys } from "i18nifty"; 2 | import { DURATION_DIVISOR_KEYS } from "core/tools/timeFormat/constants"; 3 | import type { DurationDivisorKey } from "core/tools/timeFormat/type"; 4 | 5 | export const fromNowDivisorKeys = [...DURATION_DIVISOR_KEYS, "now"] as const; 6 | export type FromNowDivisorKey = (typeof fromNowDivisorKeys)[number]; 7 | 8 | const { i18n } = declareComponentKeys< 9 | | { 10 | K: "past1"; 11 | P: { divisorKey: FromNowDivisorKey }; 12 | } 13 | | { 14 | K: "pastN"; 15 | P: { divisorKey: FromNowDivisorKey }; 16 | } 17 | | { 18 | K: "future1"; 19 | P: { divisorKey: FromNowDivisorKey }; 20 | } 21 | | { 22 | K: "futureN"; 23 | P: { divisorKey: FromNowDivisorKey }; 24 | } 25 | | { 26 | K: "singular"; 27 | P: { divisorKey: DurationDivisorKey }; 28 | } 29 | | { 30 | K: "plural"; 31 | P: { divisorKey: DurationDivisorKey }; 32 | } 33 | >()("formattedDate"); 34 | export type I18n = typeof i18n; 35 | -------------------------------------------------------------------------------- /web/src/core/ports/SecretsManager.ts: -------------------------------------------------------------------------------- 1 | export type Secret = { [key: string]: Secret.Value }; 2 | 3 | export namespace Secret { 4 | export type Value = 5 | | string 6 | | boolean 7 | | number 8 | | null 9 | | Value[] 10 | | { [key: string]: Value }; 11 | } 12 | 13 | export type SecretWithMetadata = { 14 | secret: Secret; 15 | metadata: { 16 | created_time: string; 17 | deletion_time: string | ""; 18 | destroyed: boolean; 19 | version: number; 20 | }; 21 | }; 22 | 23 | export type SecretsManager = { 24 | list: (params: { path: string }) => Promise<{ 25 | directories: string[]; 26 | files: string[]; 27 | }>; 28 | 29 | get: (params: { path: string }) => Promise; 30 | 31 | put: (params: { path: string; secret: Secret }) => Promise; 32 | 33 | delete: (params: { path: string }) => Promise; 34 | 35 | getToken: (params?: { doForceRefresh: boolean }) => Promise<{ 36 | token: string; 37 | expirationTime: number; 38 | acquisitionTime: number; 39 | }>; 40 | }; 41 | -------------------------------------------------------------------------------- /web/src/core/ports/OnyxiaApi/Language.ts: -------------------------------------------------------------------------------- 1 | import type { LocalizedString as GenericLocalizedString } from "i18nifty"; 2 | import { z } from "zod"; 3 | import { assert, type Equals } from "tsafe/assert"; 4 | import { id } from "tsafe/id"; 5 | 6 | export const languages = [ 7 | "en", 8 | "fr", 9 | "zh-CN", 10 | "no", 11 | "fi", 12 | "nl", 13 | "it", 14 | "es", 15 | "de" 16 | ] as const; 17 | 18 | export type Language = (typeof languages)[number]; 19 | 20 | export type LocalizedString = GenericLocalizedString; 21 | 22 | export const zLanguage = (() => { 23 | type TargetType = Language; 24 | 25 | const zTargetType = z.enum(languages); 26 | 27 | assert, TargetType>>(); 28 | 29 | return id>(zTargetType); 30 | })(); 31 | 32 | export const zLocalizedString = (() => { 33 | type TargetType = LocalizedString; 34 | 35 | const zTargetType = z.union([z.string(), z.record(zLanguage, z.string())]); 36 | 37 | assert, TargetType>>(); 38 | 39 | return id>(zTargetType); 40 | })(); 41 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/word.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/explorer/R.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/ui/pages/myService/MyServiceButtonBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { MyServiceButtonBar } from "./MyServiceButtonBar"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyService/MyServiceButtonBar", 7 | component: MyServiceButtonBar 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | monitoringUrl: undefined, // Monitoring URL is not provided 17 | onClickBack: action("Back button clicked"), 18 | areHelmValuesShown: false, 19 | onClickHelmValues: action("Helm Values button clicked") 20 | } 21 | }; 22 | 23 | export const WithMonitoringUrl: Story = { 24 | args: { 25 | ...Default.args, 26 | monitoringUrl: "https://example.com/monitoring" // Monitoring URL is provided 27 | } 28 | }; 29 | 30 | export const HelmValuesShown: Story = { 31 | args: { 32 | ...Default.args, 33 | areHelmValuesShown: true // Helm values are already shown 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /web/public/icons/files.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /helm-chart/templates/api/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- define "onyxia.api.configmap" }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ template "onyxia.api.fullname" . }} 6 | labels: 7 | app: {{ template "onyxia.name" . }} 8 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | data: 12 | server.servlet.context-path: {{ .Values.api.contextPath | quote }} 13 | {{- if .Values.api.env }} 14 | {{- range $key, $value := .Values.api.env }} 15 | {{ $key }}: {{ $value | quote }} 16 | {{- end -}} 17 | {{- end -}} 18 | {{- if .Values.api.regions }} 19 | regions: {{ .Values.api.regions | toJson | quote }} 20 | {{- end}} 21 | {{- if kindIs "slice" (.Values.api).catalogs }} 22 | catalogs: {{ .Values.api.catalogs | toJson | quote }} 23 | {{- end }} 24 | {{- if .Values.api.schemas.enabled }} 25 | external.schema.directory: {{ printf "%s%s/" .Values.api.schemas.rootPath .Values.api.schemas.subPathDefault | quote }} 26 | role.schema.directory: {{ printf "%s%s/" .Values.api.schemas.rootPath .Values.api.schemas.subPathRole | quote }} 27 | {{- end}} 28 | {{- end }} 29 | {{- include "onyxia.api.configmap" . }} -------------------------------------------------------------------------------- /helm-chart/templates/onboarding/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.onboarding.enabled .Values.onboarding.clusterRoleBinding.create }} 2 | {{- $clusterRoleName := "" }} 3 | {{- if .Values.onboarding.clusterRole.create }} 4 | {{- $clusterRoleName = .Values.onboarding.clusterRole.name | default (include "onyxia.onboarding.fullname" .) }} 5 | {{- else if .Values.onboarding.clusterRoleBinding.clusterRoleName }} 6 | {{- $clusterRoleName = .Values.onboarding.clusterRoleBinding.clusterRoleName }} 7 | {{- else }} 8 | {{- fail "onboarding.clusterRoleBinding.clusterRoleName is required when onboarding.clusterRole.create is false" }} 9 | {{- end }} 10 | apiVersion: rbac.authorization.k8s.io/v1 11 | kind: ClusterRoleBinding 12 | metadata: 13 | name: {{ .Values.onboarding.clusterRoleBinding.name | default (include "onyxia.onboarding.fullname" .) }} 14 | labels: 15 | {{- include "onyxia.onboarding.labels" . | nindent 4 }} 16 | roleRef: 17 | apiGroup: rbac.authorization.k8s.io 18 | kind: ClusterRole 19 | name: {{ $clusterRoleName }} 20 | subjects: 21 | - kind: ServiceAccount 22 | name: {{ include "onyxia.onboarding.serviceAccountName" . }} 23 | namespace: {{ .Release.Namespace }} 24 | {{- end }} 25 | 26 | -------------------------------------------------------------------------------- /web/public/custom-resources/my-plugin.ts: -------------------------------------------------------------------------------- 1 | 2 | // Can be transpiled to JavaScript with the following command: 3 | // node -e "require('child_process').exec('npx tsc --module commonjs --esModuleInterop false --noEmitOnError false --isolatedModules my-plugin.ts', ()=>{})" 4 | import type { Onyxia } from "../../src/pluginSystem"; 5 | 6 | window.addEventListener("onyxiaready", () => { 7 | 8 | const onyxia: Onyxia = (window as any).onyxia; 9 | 10 | onyxia.addEventListener(eventName => { 11 | switch (eventName) { 12 | case "theme updated": 13 | console.log("Onyxia theme updated: ", onyxia.theme); 14 | break; 15 | case "language changed": 16 | console.log(`Language changed to ${onyxia.lang}`); 17 | break; 18 | case "route changed": 19 | console.log(`Route changed: ${onyxia.route.name}`); 20 | break; 21 | case "route params changed": 22 | console.log(`Route params changed: `, onyxia.route.params); 23 | break; 24 | default: 25 | } 26 | }); 27 | 28 | console.log("Onyxia Global API ready", onyxia); 29 | 30 | }); -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerUploadModal/ExplorerUploadModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerUploadModal } from "./ExplorerUploadModal"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyFiles/Explorer/ExplorerUploadModal/ExplorerUploadModal", 7 | component: ExplorerUploadModal 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | isOpen: true, 17 | onClose: action("Modal closed"), 18 | filesBeingUploaded: [ 19 | { 20 | directoryPath: "/home/user/documents", 21 | basename: "file1.txt", 22 | size: 123456, 23 | uploadPercent: 50 24 | }, 25 | { 26 | directoryPath: "/home/user/pictures", 27 | basename: "image.png", 28 | size: 234567, 29 | uploadPercent: 75 30 | } 31 | ], 32 | onRequestFilesUpload: action("onRequestFileUpload") 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /web/src/ui/shared/CommandBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { CommandBar, type CommandBarProps } from "./CommandBar"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Shared/CommandBar", 7 | component: CommandBar 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | const sampleEntries: CommandBarProps.Entry[] = [ 15 | { cmdId: 1, cmd: "ls -la", resp: "file1.txt\nfile2.txt" }, 16 | { cmdId: 2, cmd: "echo 'Hello World'", resp: "Hello World" }, 17 | { cmdId: 3, cmd: "pwd", resp: "/home/user" } 18 | ]; 19 | 20 | const downloadAction = { 21 | tooltipTitle: "Download logs", 22 | onClick: () => action("Logs downloaded!") 23 | }; 24 | 25 | const helpDialog = { 26 | title: "Help", 27 | body: "This is a command bar where you can run different commands and view their responses." 28 | }; 29 | 30 | export const Default: Story = { 31 | args: { 32 | entries: sampleEntries, 33 | maxHeight: 400, 34 | downloadButton: downloadAction, 35 | helpDialog, 36 | isExpended: true 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /web/src/ui/shared/LinkFromConfig.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { LocalizedString } from "ui/i18n"; 3 | import { zLocalizedString } from "ui/i18n/z"; 4 | import { assert } from "tsafe/assert"; 5 | 6 | export type LinkFromConfig = { 7 | label: LocalizedString; 8 | url: LocalizedString; 9 | icon?: string; 10 | startIcon?: string; 11 | endIcon?: string; 12 | }; 13 | 14 | export const zLinkFromConfig = z 15 | .object({ 16 | icon: z.string().optional(), 17 | label: zLocalizedString, 18 | url: zLocalizedString, 19 | startIcon: z.string().optional(), 20 | endIcon: z.string().optional() 21 | }) 22 | .superRefine((data, ctx) => { 23 | if (data.startIcon !== undefined && data.icon !== undefined) { 24 | ctx.addIssue({ 25 | code: z.ZodIssueCode.custom, 26 | message: "You can't specify both startIcon and icon" 27 | }); 28 | } 29 | }); 30 | 31 | { 32 | type Got = ReturnType<(typeof zLinkFromConfig)["parse"]>; 33 | type Expected = LinkFromConfig; 34 | 35 | assert(); 36 | assert(); 37 | } 38 | -------------------------------------------------------------------------------- /web/src/Root.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy, useEffect } from "react"; 2 | import { useSplashScreen } from "onyxia-ui"; 3 | import { 4 | OnyxiaUi, 5 | injectCustomFontFaceIfNotAlreadyDone, 6 | loadThemedFavicon 7 | } from "ui/theme"; 8 | 9 | injectCustomFontFaceIfNotAlreadyDone(); 10 | loadThemedFavicon(); 11 | 12 | const App = lazy(() => import("ui/App")); 13 | 14 | export function Root() { 15 | return ( 16 | 17 | }> 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | function AppWrapper() { 25 | const { hideRootSplashScreen } = useSplashScreen(); 26 | 27 | useEffect(() => { 28 | hideRootSplashScreen(); 29 | }, []); 30 | 31 | return ; 32 | } 33 | 34 | function SplashScreen() { 35 | const { showSplashScreen, hideSplashScreen, hideRootSplashScreen } = 36 | useSplashScreen(); 37 | 38 | useEffect(() => { 39 | hideRootSplashScreen(); 40 | showSplashScreen({ enableTransparency: false }); 41 | 42 | return () => { 43 | hideSplashScreen(); 44 | }; 45 | }, []); 46 | 47 | return null; 48 | } 49 | -------------------------------------------------------------------------------- /web/src/core/tools/Stringifyable/assignValueAtPath.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { assignValueAtPath } from "./assignValueAtPath"; 3 | import { symToStr } from "tsafe/symToStr"; 4 | 5 | describe(symToStr({ assignValueAtPath }), () => { 6 | it("works with base case", () => { 7 | const object = { 8 | a: [{ b: null }] 9 | }; 10 | 11 | assignValueAtPath({ 12 | stringifyableObjectOrArray: object, 13 | path: ["a", 0, "b"], 14 | value: [1, 2, 3] 15 | }); 16 | 17 | expect(object).toStrictEqual({ 18 | a: [ 19 | { 20 | b: [1, 2, 3] 21 | } 22 | ] 23 | }); 24 | }); 25 | 26 | it("create sub object/array", () => { 27 | const object = {}; 28 | 29 | assignValueAtPath({ 30 | stringifyableObjectOrArray: object, 31 | path: ["a", 0, "b"], 32 | value: [1, 2, 3] 33 | }); 34 | 35 | expect(object).toStrictEqual({ 36 | a: [ 37 | { 38 | b: [1, 2, 3] 39 | } 40 | ] 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/Directory.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /web/src/ui/pages/launcher/RootFormComponent/FormFieldGroupComponent/AutoInjectSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from "onyxia-ui/IconButton"; 2 | import { getIconUrlByName } from "lazy-icons"; 3 | import { Tooltip } from "onyxia-ui/Tooltip"; 4 | import { declareComponentKeys, useTranslation } from "ui/i18n"; 5 | 6 | type Props = { 7 | className?: string; 8 | isAutoInjected: boolean; 9 | onChange: (isAutoInjected: boolean) => void; 10 | }; 11 | 12 | export function AutoInjectSwitch(props: Props) { 13 | const { className, isAutoInjected, onChange } = props; 14 | 15 | const { t } = useTranslation({ AutoInjectSwitch }); 16 | 17 | return ( 18 | 19 | onChange(!isAutoInjected)} 22 | icon={getIconUrlByName(isAutoInjected ? "Power" : "PowerOff")} 23 | size="extra small" 24 | /> 25 | 26 | ); 27 | } 28 | 29 | const { i18n } = declareComponentKeys<{ 30 | K: "tooltip"; 31 | P: { isAutoInjected: boolean }; 32 | R: JSX.Element; 33 | }>()({ AutoInjectSwitch }); 34 | export type I18n = typeof i18n; 35 | -------------------------------------------------------------------------------- /web/src/ui/tools/renderStringWithHighlights.tsx: -------------------------------------------------------------------------------- 1 | import { id } from "tsafe/id"; 2 | 3 | export type StringWithHighlights = { 4 | charArray: string[]; 5 | highlightedIndexes: number[]; 6 | }; 7 | 8 | export function renderStringWithHighlights(params: { 9 | stringWithHighlights: StringWithHighlights; 10 | doCapitalize: boolean; 11 | highlightedCharClassName: string; 12 | }): JSX.Element { 13 | const { 14 | stringWithHighlights: { charArray, highlightedIndexes }, 15 | doCapitalize, 16 | highlightedCharClassName 17 | } = params; 18 | 19 | return ( 20 | <> 21 | {charArray 22 | .map( 23 | !doCapitalize 24 | ? id 25 | : (char, i) => (i === 0 ? char.toUpperCase() : char) 26 | ) 27 | .map((char, i) => 28 | highlightedIndexes.includes(i) ? ( 29 | 30 | {char} 31 | 32 | ) : ( 33 | char 34 | ) 35 | )} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerButtonBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerButtonBar } from "./ExplorerButtonBar"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyFiles/Explorer/ExplorerButtonBar", 7 | component: ExplorerButtonBar 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | args: { 16 | selectedItemKind: "none", 17 | callback: action("Button clicked"), 18 | onViewModeChange: action("onViewModeChange"), 19 | viewMode: "list" 20 | } 21 | }; 22 | 23 | export const FileSelected: Story = { 24 | args: { 25 | selectedItemKind: "file", 26 | callback: action("Button clicked"), 27 | onViewModeChange: action("onViewModeChange"), 28 | viewMode: "list" 29 | } 30 | }; 31 | 32 | export const DirectorySelected: Story = { 33 | args: { 34 | selectedItemKind: "directory", 35 | callback: action("Button clicked"), 36 | onViewModeChange: action("onViewModeChange"), 37 | viewMode: "list" 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/route.ts: -------------------------------------------------------------------------------- 1 | import { id } from "tsafe"; 2 | import { 3 | defineRoute, 4 | createGroup, 5 | param, 6 | type ValueSerializer, 7 | noMatch 8 | } from "type-route"; 9 | import { type ViewMode, viewModes } from "./shared/types"; 10 | 11 | export const routeDefs = { 12 | myFiles: defineRoute( 13 | { 14 | path: param.path.trailing.ofType({ 15 | parse: raw => decodeURIComponent(raw), // decode the path 16 | stringify: value => encodeURI(value) // encode when creating URL 17 | }), 18 | mode: param.query.optional 19 | .ofType( 20 | id>({ 21 | parse: raw => 22 | !id(viewModes).includes(raw) 23 | ? noMatch 24 | : (raw as ViewMode), 25 | stringify: value => value 26 | }) 27 | ) 28 | .default(viewModes[0]) 29 | }, 30 | ({ path }) => [`/file-explorer/${path}`, `/my-files/${path}`] 31 | ) 32 | }; 33 | 34 | export const routeGroup = createGroup(routeDefs); 35 | -------------------------------------------------------------------------------- /web/src/ui/pages/projectSettings/ProjectSettingsSecurityInfosTab.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { SettingField } from "ui/shared/SettingField"; 3 | import { useCoreState, getCoreSync } from "core"; 4 | import { copyToClipboard } from "ui/tools/copyToClipboard"; 5 | 6 | export type Props = { 7 | className?: string; 8 | }; 9 | 10 | export const ProjectSettingsSecurityInfosTab = memo((props: Props) => { 11 | const { className } = props; 12 | 13 | const { 14 | functions: { projectManagement } 15 | } = getCoreSync(); 16 | 17 | const servicePassword = useCoreState("projectManagement", "servicePassword"); 18 | const groupProjectName = useCoreState("projectManagement", "groupProjectName"); 19 | 20 | return ( 21 |
22 | copyToClipboard(servicePassword)} 27 | onRequestServicePasswordRenewal={() => 28 | projectManagement.renewServicePassword() 29 | } 30 | /> 31 |
32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /web/src/core/usecases/projectManagement/state.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from "core/ports/OnyxiaApi"; 2 | import { 3 | createUsecaseActions, 4 | createObjectThatThrowsIfAccessed 5 | } from "clean-architecture"; 6 | import type { ProjectConfigs } from "./decoupledLogic/ProjectConfigs"; 7 | 8 | type State = { 9 | projects: (Project & { doInjectPersonalInfos: boolean })[]; 10 | selectedProjectId: string; 11 | currentProjectConfigs: ProjectConfigs; 12 | }; 13 | 14 | export const name = "projectManagement"; 15 | 16 | export const { reducer, actions } = createUsecaseActions({ 17 | name, 18 | initialState: createObjectThatThrowsIfAccessed(), 19 | reducers: { 20 | projectChanged: (_state, { payload }: { payload: State }) => payload, 21 | configValueUpdated: ( 22 | state, 23 | { payload }: { payload: ChangeConfigValueParams } 24 | ) => { 25 | const { key, value } = payload; 26 | 27 | Object.assign(state.currentProjectConfigs, { [key]: value }); 28 | } 29 | } 30 | }); 31 | 32 | export type ChangeConfigValueParams< 33 | K extends keyof ProjectConfigs = keyof ProjectConfigs 34 | > = { 35 | key: K; 36 | value: ProjectConfigs[K]; 37 | }; 38 | -------------------------------------------------------------------------------- /helm-chart/templates/api/route.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.route.enabled -}} 2 | {{- $fullNameApi := include "onyxia.api.fullname" . -}} 3 | {{- $fullName := include "onyxia.fullname" . -}} 4 | apiVersion: route.openshift.io/v1 5 | kind: Route 6 | metadata: 7 | name: {{ $fullName }}-api 8 | labels: 9 | {{- include "onyxia.labels" . | nindent 4 }} 10 | annotations: 11 | {{- with .Values.route.annotations }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | host: {{ .Values.route.host | quote }} 16 | path: /api 17 | to: 18 | kind: Service 19 | name: {{ $fullNameApi }} 20 | tls: 21 | termination: {{ .Values.route.tls.termination }} 22 | {{- if .Values.route.tls.key }} 23 | key: {{- .Values.route.tls.key }} 24 | {{- end }} 25 | {{- if .Values.route.tls.certificate }} 26 | certificate: {{- .Values.route.tls.certificate }} 27 | {{- end }} 28 | {{- if .Values.route.tls.caCertificate }} 29 | caCertificate: {{- .Values.route.tls.caCertificate }} 30 | {{- end }} 31 | {{- if .Values.route.tls.destinationCACertificate }} 32 | destinationCACertificate: {{- .Values.route.tls.destinationCACertificate }} 33 | {{- end }} 34 | wildcardPolicy: {{ .Values.route.wildcardPolicy }} 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /helm-chart/templates/web/route.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.route.enabled -}} 2 | {{- $fullNameWeb := include "onyxia.web.fullname" . -}} 3 | {{- $fullName := include "onyxia.fullname" . -}} 4 | apiVersion: route.openshift.io/v1 5 | kind: Route 6 | metadata: 7 | name: {{ $fullName }}-web 8 | labels: 9 | {{- include "onyxia.labels" . | nindent 4 }} 10 | annotations: 11 | {{- with .Values.route.annotations }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | host: {{ .Values.route.host | quote }} 16 | path: / 17 | to: 18 | kind: Service 19 | name: {{ $fullNameWeb }} 20 | tls: 21 | termination: {{ .Values.route.tls.termination }} 22 | {{- if .Values.route.tls.key }} 23 | key: {{- .Values.route.tls.key }} 24 | {{- end }} 25 | {{- if .Values.route.tls.certificate }} 26 | certificate: {{- .Values.route.tls.certificate }} 27 | {{- end }} 28 | {{- if .Values.route.tls.caCertificate }} 29 | caCertificate: {{- .Values.route.tls.caCertificate }} 30 | {{- end }} 31 | {{- if .Values.route.tls.destinationCACertificate }} 32 | destinationCACertificate: {{- .Values.route.tls.destinationCACertificate }} 33 | {{- end }} 34 | wildcardPolicy: {{ .Values.route.wildcardPolicy }} 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /web/src/core/tools/bytes.ts: -------------------------------------------------------------------------------- 1 | import bytes from "bytes"; 2 | 3 | export function toBytes(str: string) { 4 | const validBytesInput = (() => { 5 | const s = str.trim(); 6 | 7 | if (s === "0") { 8 | return "0b"; 9 | } 10 | 11 | if (/^\d+$/.test(s)) { 12 | return s + "b"; 13 | } 14 | 15 | if (s.endsWith("B")) { 16 | return s.toLowerCase(); 17 | } 18 | 19 | if (s.endsWith("Ki") || s.endsWith("K")) { 20 | return s.slice(0, -2) + "kb"; 21 | } 22 | 23 | if (s.endsWith("Mi") || s.endsWith("M")) { 24 | return s.slice(0, -2) + "mb"; 25 | } 26 | 27 | if (s.endsWith("Gi") || s.endsWith("G")) { 28 | return s.slice(0, -2) + "gb"; 29 | } 30 | 31 | if (s.endsWith("Ti") || s.endsWith("T")) { 32 | return s.slice(0, -2) + "tb"; 33 | } 34 | 35 | if (s.endsWith("Pi") || s.endsWith("P")) { 36 | return s.slice(0, -2) + "pb"; 37 | } 38 | 39 | return undefined; 40 | })(); 41 | 42 | if (validBytesInput === undefined) { 43 | throw new Error(`${str} is not a valid bytes input`); 44 | } 45 | 46 | return bytes(validBytesInput); 47 | } 48 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 |

2 | 🖥  The Web Application  🖥️ 3 |
4 |
5 | Interested in Contributing? Take a look at our technical documentation 6 |
7 |
8 |

9 | 10 | ## Overview 11 | 12 | This repository contains the source code for the Docker image [inseefrlab/onyxia-web](https://hub.docker.com/r/inseefrlab/onyxia-web). 13 | 14 | ## Architecture 15 | 16 | `onyxia-web` is a Vite Single Page Application (SPA) that runs entirely in the user's browser and is delivered as static files. 17 | 18 | ## Project Structure 19 | 20 | - **UI Layer:** This project utilizes React, but solely as a UI library. The React-specific code is isolated to [src/ui](./src/ui). 21 | - **Core Logic:** The bulk of the application's functionality resides in [src/core](./src/core). Importantly, the core logic is entirely agnostic to React. 22 | 23 | ## Run the Docker image locally 24 | 25 | ```bash 26 | docker build -t inseefrlab/onyxia-web:main . 27 | docker run -it -p 8083:8080 --env ONYXIA_API_URL='https://datalab.sspcloud.fr/api' inseefrlab/onyxia-web:main 28 | ``` 29 | 30 | Navigate to http://localhost:8083 in your browser. 31 | -------------------------------------------------------------------------------- /web/src/ui/shared/ensureUrlIsSafe.ts: -------------------------------------------------------------------------------- 1 | import { getSafeUrl as getSafeUrl_base } from "onyxia-ui/tools/getSafeUrl"; 2 | import { assert } from "tsafe/assert"; 3 | 4 | /** Throws if urls isn't safe, returns url */ 5 | export function ensureUrlIsSafe(url: string): void { 6 | try { 7 | getSafeUrl_base(url); 8 | } catch { 9 | throw new Error(`Invalid url: ${url}`); 10 | } 11 | 12 | if (url.startsWith("/")) { 13 | return; 14 | } 15 | 16 | if (window.kcContext === undefined) { 17 | throw new Error(`${url} is not a local url. (Local urls start with "/")`); 18 | } 19 | 20 | const { ONYXIA_RESOURCES_ALLOWED_ORIGINS } = window.kcContext.properties; 21 | 22 | assert(typeof ONYXIA_RESOURCES_ALLOWED_ORIGINS === "string"); 23 | 24 | if (ONYXIA_RESOURCES_ALLOWED_ORIGINS === "*") { 25 | return; 26 | } 27 | 28 | const safeOriginOfUrl = ONYXIA_RESOURCES_ALLOWED_ORIGINS.split(",") 29 | .map(origin => origin.trim()) 30 | .find(origin => url.startsWith(origin.toLowerCase())); 31 | 32 | if (safeOriginOfUrl === undefined) { 33 | throw new Error( 34 | `${url} is not from an allowed origin, allowed origins: ${ONYXIA_RESOURCES_ALLOWED_ORIGINS}` 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/src/ui/pages/document/Page.tsx: -------------------------------------------------------------------------------- 1 | import { LocalizedMarkdown } from "ui/shared/Markdown"; 2 | import { tss } from "tss"; 3 | import { useRoute } from "ui/routes"; 4 | import { routeGroup } from "./route"; 5 | import { assert } from "tsafe"; 6 | 7 | const Page = Document; 8 | export default Page; 9 | 10 | function Document() { 11 | const route = useRoute(); 12 | assert(routeGroup.has(route)); 13 | 14 | const { classes } = useStyles(); 15 | 16 | return ( 17 |
18 | 19 | {route.params.source} 20 | 21 |
22 | ); 23 | } 24 | 25 | const useStyles = tss.withName({ Document }).create(({ theme }) => ({ 26 | root: { 27 | display: "flex", 28 | justifyContent: "center", 29 | height: "100%" 30 | }, 31 | markdown: { 32 | borderRadius: theme.spacing(2), 33 | backgroundColor: theme.colors.useCases.surfaces.surface1, 34 | maxWidth: 900, 35 | padding: theme.spacing(4), 36 | "&:hover": { 37 | boxShadow: theme.shadows[1] 38 | }, 39 | marginBottom: theme.spacing(2), 40 | overflow: "auto" 41 | } 42 | })); 43 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/ShareFile/SelectTime.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { SelectTime } from "./SelectTime"; 3 | import { useState } from "react"; 4 | 5 | const meta: Meta = { 6 | title: "Pages/MyFiles/ShareFile/SelectTime", 7 | component: SelectTime 8 | }; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = { 15 | render: args => { 16 | const [expirationValue, setExpirationValue] = useState( 17 | args.validityDurationSecond 18 | ); 19 | 20 | const handleExpirationValueChange = (props: { 21 | validityDurationSecond: number; 22 | }) => { 23 | const { validityDurationSecond } = props; 24 | setExpirationValue(validityDurationSecond); 25 | }; 26 | 27 | return ( 28 | 33 | ); 34 | }, 35 | args: { 36 | validityDurationSecondOptions: [3600, 7200, 10800] // Example options: 1h, 2h, 3h 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /web/src/ui/pages/fileExplorer/Explorer/ExplorerUploadModal/ExplorerUploadProgress.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExplorerUploadProgress } from "./ExplorerUploadProgress"; 3 | import { action } from "@storybook/addon-actions"; 4 | 5 | const meta = { 6 | title: "Pages/MyFiles/Explorer/ExplorerUploadModal/ExplorerUploadProgress", 7 | component: ExplorerUploadProgress 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const UploadInProgress: Story = { 15 | args: { 16 | basename: "example-file.txt", 17 | percentUploaded: 50, 18 | fileSize: 1024 * 1024 * 5, // 5 MB 19 | isFailed: false 20 | } 21 | }; 22 | 23 | export const UploadFailed: Story = { 24 | args: { 25 | basename: "example-file.txt", 26 | percentUploaded: 75, 27 | fileSize: 1024 * 1024 * 5, // 5 MB 28 | isFailed: true, 29 | onClick: action("Upload failed action") 30 | } 31 | }; 32 | 33 | export const UploadComplete: Story = { 34 | args: { 35 | basename: "example-file.txt", 36 | percentUploaded: 100, 37 | fileSize: 1024 * 1024 * 5, // 5 MB 38 | isFailed: false 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /web/src/ui/shared/formattedDate/useFormattedDate.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useEffect, useState } from "react"; 2 | import { useLang } from "ui/i18n"; 3 | import { getFormattedDate } from "./getFormattedDate"; 4 | import { fromNow } from "./dateTimeFormatter"; 5 | 6 | export function useFormattedDate(params: { time: number }): string { 7 | const { time } = params; 8 | 9 | // NOTE: So that we get a refresh when the lang is changed. 10 | const { lang } = useLang(); 11 | 12 | return useMemo(() => getFormattedDate({ time, lang }), [time, lang]); 13 | } 14 | 15 | export function useFromNow(params: { dateTime: number }) { 16 | const { dateTime } = params; 17 | 18 | const { lang } = useLang(); 19 | 20 | const [fromNowText, setFromNowText] = useState(() => fromNow({ dateTime })); 21 | 22 | useEffect(() => { 23 | const updateText = () => { 24 | const newText = fromNow({ dateTime }); 25 | 26 | if (fromNowText !== newText) { 27 | setFromNowText(newText); 28 | } 29 | }; 30 | 31 | updateText(); 32 | 33 | const timer = setInterval(updateText, 1000); 34 | 35 | return () => { 36 | clearInterval(timer); 37 | }; 38 | }, [dateTime, lang]); 39 | 40 | return { fromNowText }; 41 | } 42 | -------------------------------------------------------------------------------- /web/src/ui/shared/textEditor/DataTextEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy, Suspense } from "react"; 2 | import { assert, type Equals } from "tsafe/assert"; 3 | import type { Stringifyable } from "core/tools/Stringifyable"; 4 | 5 | export type Props = { 6 | className?: string; 7 | id?: string; 8 | maxHeight?: number; 9 | fallback?: JSX.Element; 10 | value: Stringifyable; 11 | onChange: (newValue: Stringifyable) => void; 12 | onErrorMsgChanged?: (errorMsg: string | undefined) => void; 13 | jsonSchema: Record | undefined; 14 | additionalValidation?: ( 15 | value: Stringifyable 16 | ) => { isValid: true } | { isValid: false; errorMsg: string }; 17 | allDefaults?: { 18 | isChecked: boolean; 19 | onIsCheckedChange: (isChecked: boolean) => void; 20 | }; 21 | 22 | isJson5Enabled?: boolean; 23 | }; 24 | 25 | { 26 | type Props_Expected = import("./DataTextEditor").Props; 27 | 28 | assert>(); 29 | } 30 | 31 | const DataTextEditorLazy = lazy(() => import("./DataTextEditor")); 32 | 33 | export function DataTextEditor(props: Props) { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /web/src/ui/assets/svg/ServiceNotFound.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/ui/pages/document/route.ts: -------------------------------------------------------------------------------- 1 | import { defineRoute, param, noMatch, createGroup } from "type-route"; 2 | import type { ValueSerializer } from "type-route"; 3 | import { id } from "tsafe/id"; 4 | import { type LocalizedString, zLocalizedString } from "ui/i18n"; 5 | 6 | export const routeDefs = { 7 | document: defineRoute( 8 | { 9 | source: param.query.ofType( 10 | id>({ 11 | parse: str => { 12 | let value: LocalizedString; 13 | 14 | try { 15 | value = JSON.parse(decodeURIComponent(str)); 16 | } catch { 17 | return noMatch; 18 | } 19 | 20 | try { 21 | zLocalizedString.parse(value); 22 | } catch { 23 | return noMatch; 24 | } 25 | 26 | return value; 27 | }, 28 | stringify: value => encodeURIComponent(JSON.stringify(value)) 29 | }) 30 | ) 31 | }, 32 | () => `/document` 33 | ) 34 | }; 35 | 36 | export const routeGroup = createGroup(routeDefs); 37 | -------------------------------------------------------------------------------- /web/src/ui/pages/home/LinkFromConfigButton.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { Button, type ButtonProps } from "onyxia-ui/Button"; 3 | import { useUrlToLink } from "ui/routes"; 4 | import type { LinkFromConfig } from "ui/shared/LinkFromConfig"; 5 | import { LocalizedMarkdown } from "ui/shared/Markdown"; 6 | import { getIconUrl } from "lazy-icons"; 7 | 8 | type Props = { 9 | className?: string; 10 | linkFromConfig: LinkFromConfig; 11 | variant?: ButtonProps["variant"]; 12 | }; 13 | 14 | export function LinkFromConfigButton(props: Props) { 15 | const { className, linkFromConfig, variant } = props; 16 | 17 | const { label, url, icon, startIcon, endIcon } = linkFromConfig; 18 | 19 | const { urlToLink } = useUrlToLink(); 20 | 21 | const link = useMemo(() => urlToLink(url), [urlToLink]); 22 | 23 | return ( 24 | 35 | ); 36 | } 37 | --------------------------------------------------------------------------------