diff --git a/web/src/core/adapters/s3Client/s3Client.ts b/web/src/core/adapters/s3Client/s3Client.ts index 16ecec2f3..64617d95d 100644 --- a/web/src/core/adapters/s3Client/s3Client.ts +++ b/web/src/core/adapters/s3Client/s3Client.ts @@ -594,6 +594,23 @@ export function createS3Client( ); return downloadUrl; + }, + + headObject: async ({ path }) => { + const { bucketName, objectName } = bucketNameAndObjectNameFromS3Path(path); + + const { getAwsS3Client } = await prApi; + + const { awsS3Client } = await getAwsS3Client(); + + const head = await awsS3Client.send( + new (await import("@aws-sdk/client-s3")).HeadObjectCommand({ + Bucket: bucketName, + Key: objectName + }) + ); + + return { contentType: head.ContentType, metadata: head.Metadata }; } // "getPresignedUploadUrl": async ({ path, validityDurationSecond }) => { diff --git a/web/src/core/ports/S3Client.ts b/web/src/core/ports/S3Client.ts index 50eaef238..7b7c29570 100644 --- a/web/src/core/ports/S3Client.ts +++ b/web/src/core/ports/S3Client.ts @@ -59,6 +59,11 @@ export type S3Client = { validityDurationSecond: number; }) => Promise; + headObject: (params: { path: string }) => Promise<{ + contentType: string | undefined; + metadata: Record | undefined; + }>; + // getPresignedUploadUrl: (params: { // path: string; // validityDurationSecond: number; diff --git a/web/src/core/usecases/dataExplorer/state.ts b/web/src/core/usecases/dataExplorer/state.ts index 1875cad36..a03ba275f 100644 --- a/web/src/core/usecases/dataExplorer/state.ts +++ b/web/src/core/usecases/dataExplorer/state.ts @@ -25,6 +25,7 @@ export type State = { rows: any[]; rowCount: number | undefined; fileDownloadUrl: string; + // fileType: "parquet" | "csv" | "json"; } | undefined; }; diff --git a/web/src/core/usecases/dataExplorer/thunks.ts b/web/src/core/usecases/dataExplorer/thunks.ts index 2969d6b47..454bd147e 100644 --- a/web/src/core/usecases/dataExplorer/thunks.ts +++ b/web/src/core/usecases/dataExplorer/thunks.ts @@ -7,6 +7,46 @@ import { assert } from "tsafe/assert"; import * as s3ConfigManagement from "core/usecases/s3ConfigManagement"; const privateThunks = { + getFileDonwloadUrl: + (params: { sourceUrl: string }) => + async (...args) => { + const [dispatch, , { oidc }] = args; + + const { sourceUrl } = params; + + const fileDownloadUrl = await (async () => { + if (sourceUrl.startsWith("https://")) { + return sourceUrl; + } + + const s3path = sourceUrl.replace(/^s3:\/\//, "/"); + assert(s3path !== sourceUrl, "Unsupported protocol"); + + if (!oidc.isUserLoggedIn) { + oidc.login({ doesCurrentHrefRequiresAuth: true }); + await new Promise(() => {}); + } + + const s3Client = ( + await dispatch( + s3ConfigManagement.protectedThunks.getS3ConfigAndClientForExplorer() + ) + )?.s3Client; + + if (s3Client === undefined) { + alert("No S3 client available"); + await new Promise(() => {}); + assert(false); + } + + return s3Client.getFileDownloadUrl({ + path: s3path, + validityDurationSecond: 3600 * 6 + }); + })(); + + return fileDownloadUrl; + }, performQuery: (params: { queryParams: { @@ -28,7 +68,7 @@ const privateThunks = { dispatch(actions.queryStarted({ queryParams })); - const { sqlOlap, oidc } = rootContext; + const { sqlOlap } = rootContext; const getIsActive = () => same(getState()[name].queryParams, queryParams); @@ -49,36 +89,11 @@ const privateThunks = { }; } - const fileDownloadUrl = await (async () => { - if (sourceUrl.startsWith("https://")) { - return sourceUrl; - } - - const s3path = sourceUrl.replace(/^s3:\/\//, "/"); - assert(s3path !== sourceUrl, "Unsupported protocol"); - - if (!oidc.isUserLoggedIn) { - oidc.login({ doesCurrentHrefRequiresAuth: true }); - await new Promise(() => {}); - } - - const s3Client = ( - await dispatch( - s3ConfigManagement.protectedThunks.getS3ConfigAndClientForExplorer() - ) - )?.s3Client; - - if (s3Client === undefined) { - alert("No S3 client available"); - await new Promise(() => {}); - assert(false); - } - - return s3Client.getFileDownloadUrl({ - path: s3path, - validityDurationSecond: 3600 * 6 - }); - })(); + const fileDownloadUrl = await dispatch( + privateThunks.getFileDonwloadUrl({ + sourceUrl + }) + ); const rowCountOrErrorMessage = await sqlOlap .getRowCount(sourceUrl) @@ -132,6 +147,49 @@ const privateThunks = { fileDownloadUrl }) ); + }, + detectFileType: + (params: { sourceUrl: string }) => + async (...args) => { + const { sourceUrl } = params; + const [dispatch] = args; + + const extension = (() => { + const validExtensions = ["parquet", "csv", "json"] as const; + type ValidExtension = (typeof validExtensions)[number]; + + const isValidExtension = (ext: string): ext is ValidExtension => + validExtensions.includes(ext as ValidExtension); + + let pathname: string; + + try { + pathname = new URL(sourceUrl).pathname; + } catch { + return undefined; + } + const match = pathname.match(/\.(\w+)$/); + + if (match === null) { + return undefined; + } + + const [, extension] = match; + + return isValidExtension(extension) ? extension : undefined; + })(); + + if (extension) { + return extension; + } + + const contentType = await (async () => { + const fileDownloadUrl = await dispatch( + privateThunks.getFileDonwloadUrl({ + sourceUrl + }) + ); + })(); } } satisfies Thunks; diff --git a/web/src/ui/i18n/resources/de.tsx b/web/src/ui/i18n/resources/de.tsx index 60979628a..72ea9ee65 100644 --- a/web/src/ui/i18n/resources/de.tsx +++ b/web/src/ui/i18n/resources/de.tsx @@ -969,7 +969,8 @@ Fühlen Sie sich frei, Ihre Kubernetes-Bereitstellungen zu erkunden und die Kont "resize table": undefined }, UrlInput: { - load: "Laden" + load: "Laden", + reset: "Leeren" }, CommandBar: { ok: "Ok" diff --git a/web/src/ui/i18n/resources/en.tsx b/web/src/ui/i18n/resources/en.tsx index ab00c0a2f..2537739b1 100644 --- a/web/src/ui/i18n/resources/en.tsx +++ b/web/src/ui/i18n/resources/en.tsx @@ -299,7 +299,9 @@ export const translations: Translations<"en"> = { our documentation .   - Configure the minio clients. + + Configure the minio clients + . ) }, @@ -345,7 +347,9 @@ export const translations: Translations<"en"> = { our documentation .   - Configure your local Vault CLI. + + Configure your local Vault CLI + . ) }, @@ -950,7 +954,8 @@ Feel free to explore and take charge of your Kubernetes deployments! "resize table": "Resize" }, UrlInput: { - load: "Load" + load: "Load", + reset: "Reset" }, CommandBar: { ok: "Ok" diff --git a/web/src/ui/i18n/resources/es.tsx b/web/src/ui/i18n/resources/es.tsx index 2b912e6af..a284f60cf 100644 --- a/web/src/ui/i18n/resources/es.tsx +++ b/web/src/ui/i18n/resources/es.tsx @@ -311,7 +311,9 @@ export const translations: Translations<"en"> = { nuestra documentación .   - Configura los clientes de minio. + + Configura los clientes de minio + . ) }, @@ -357,7 +359,9 @@ export const translations: Translations<"en"> = { nuestra documentación .   - Configura tu Vault CLI local. + + Configura tu Vault CLI local + . ) }, @@ -955,7 +959,9 @@ export const translations: Translations<"en"> = { específica del archivo copiando la URL de la barra de direcciones.
¿No estás seguro por dónde empezar? ¡Prueba este{" "} - archivo de demostración! + + archivo de demostración + ! ), column: "columna", @@ -964,7 +970,8 @@ export const translations: Translations<"en"> = { "resize table": "Redimensionar" }, UrlInput: { - load: "Cargar" + load: "Cargar", + reset: "Vaciar" }, CommandBar: { ok: "Aceptar" diff --git a/web/src/ui/i18n/resources/fi.tsx b/web/src/ui/i18n/resources/fi.tsx index 2e4bac931..daa182471 100644 --- a/web/src/ui/i18n/resources/fi.tsx +++ b/web/src/ui/i18n/resources/fi.tsx @@ -353,7 +353,9 @@ export const translations: Translations<"fi"> = { dokumentaatiomme .   - Määritä paikallinen Vault CLI. + + Määritä paikallinen Vault CLI + . ) }, @@ -956,7 +958,8 @@ Tutustu vapaasti ja ota hallintaan Kubernetes-julkaisusi! "resize table": "Muuta taulukon kokoa" }, UrlInput: { - load: "Lataa" + load: "Lataa", + reset: "Tyhjennä" }, CommandBar: { ok: "ok" diff --git a/web/src/ui/i18n/resources/fr.tsx b/web/src/ui/i18n/resources/fr.tsx index 93ff72bee..56fa7e06a 100644 --- a/web/src/ui/i18n/resources/fr.tsx +++ b/web/src/ui/i18n/resources/fr.tsx @@ -313,7 +313,9 @@ export const translations: Translations<"fr"> = { notre documentation .   - Configurer les clients MinIO. + + Configurer les clients MinIO + . ) }, @@ -359,7 +361,9 @@ export const translations: Translations<"fr"> = { notre documentation .   - Configurer votre Vault CLI local. + + Configurer votre Vault CLI local + . ) }, @@ -965,7 +969,9 @@ N'hésitez pas à explorer et à prendre en main vos déploiements Kubernetes ! spécifique du fichier en copiant l'URL de la barre d'adresse.
Vous ne savez pas par où commencer ? Essayez ce{" "} - fichier de démonstration ! + + fichier de démonstration + ! ), column: "colonne", @@ -974,7 +980,8 @@ N'hésitez pas à explorer et à prendre en main vos déploiements Kubernetes ! "resize table": "Redimensionner" }, UrlInput: { - load: "Charger" + load: "Charger", + reset: "Vider" }, CommandBar: { ok: "ok" diff --git a/web/src/ui/i18n/resources/it.tsx b/web/src/ui/i18n/resources/it.tsx index c187f5c7f..e951b0654 100644 --- a/web/src/ui/i18n/resources/it.tsx +++ b/web/src/ui/i18n/resources/it.tsx @@ -308,7 +308,9 @@ export const translations: Translations<"it"> = { la nostra documentazione .   - Configurare i client MinIO. + + Configurare i client MinIO + . ) }, @@ -354,8 +356,9 @@ export const translations: Translations<"it"> = { la nostra documentazione .   - Configurare il tuo Vault CLI locale - . + + Configurare il tuo Vault CLI locale + . ) }, @@ -965,7 +968,8 @@ Sentiti libero di esplorare e prendere il controllo dei tuoi deployment Kubernet "resize table": "Ridimensiona" }, UrlInput: { - load: "Carica" + load: "Carica", + reset: "Svuotare" }, CommandBar: { ok: "ok" diff --git a/web/src/ui/i18n/resources/nl.tsx b/web/src/ui/i18n/resources/nl.tsx index 23f8ae951..dc064ae75 100644 --- a/web/src/ui/i18n/resources/nl.tsx +++ b/web/src/ui/i18n/resources/nl.tsx @@ -356,7 +356,9 @@ export const translations: Translations<"nl"> = { onze documentatie .   - Uw lokale Vault CLI instellen. + + Uw lokale Vault CLI instellen + . ) }, @@ -967,7 +969,8 @@ Voel je vrij om te verkennen en de controle over je Kubernetes-implementaties te "resize table": "Formaat wijzigen" }, UrlInput: { - load: "Laden" + load: "Laden", + reset: "Leegmaken" }, CommandBar: { ok: "ok" diff --git a/web/src/ui/i18n/resources/no.tsx b/web/src/ui/i18n/resources/no.tsx index 59b88d8fe..bd2fc53f7 100644 --- a/web/src/ui/i18n/resources/no.tsx +++ b/web/src/ui/i18n/resources/no.tsx @@ -961,7 +961,8 @@ Utforsk gjerne og ta kontroll over tjenestene du kjører på Kubernetes! "resize table": "Endre størrelse" }, UrlInput: { - load: "Last" + load: "Last", + reset: "Tøm" }, CommandBar: { ok: "ok" diff --git a/web/src/ui/i18n/resources/zh-CN.tsx b/web/src/ui/i18n/resources/zh-CN.tsx index 7a9c79024..2a62a6864 100644 --- a/web/src/ui/i18n/resources/zh-CN.tsx +++ b/web/src/ui/i18n/resources/zh-CN.tsx @@ -817,7 +817,9 @@ ${ "share tooltip - belong to you, not shared": ({ projectName, focusColor }) => ( <> 只有您可以访问此服务。点击共享给 - {projectName}项目成员。 + + {projectName} + 项目成员。 ) }, @@ -892,7 +894,9 @@ ${ URL,分享文件的永久链接,甚至是文件中某个特定行的链接。
不知道从哪里开始?尝试这个{" "} - 演示文件! + + 演示文件 + ! ), column: "列", @@ -901,7 +905,8 @@ ${ "resize table": "调整大小" }, UrlInput: { - load: "加载" + load: "加载", + reset: "清空" }, CommandBar: { ok: "是" diff --git a/web/src/ui/pages/dataExplorer/DataExplorer.tsx b/web/src/ui/pages/dataExplorer/DataExplorer.tsx index 7c3d1ed77..b1e7b1e4e 100644 --- a/web/src/ui/pages/dataExplorer/DataExplorer.tsx +++ b/web/src/ui/pages/dataExplorer/DataExplorer.tsx @@ -5,7 +5,7 @@ import { routes } from "ui/routes"; import { useCore, useCoreState } from "core"; import { Alert } from "onyxia-ui/Alert"; import { CircularProgress } from "onyxia-ui/CircularProgress"; -import { assert, type Equals } from "tsafe/assert"; +import { assert } from "tsafe/assert"; import { UrlInput } from "./UrlInput"; import { PageHeader } from "onyxia-ui/PageHeader"; import { getIconUrlByName } from "lazy-icons"; @@ -198,7 +198,7 @@ export default function DataExplorer(props: Props) { disableColumnMenu loading={isQuerying} paginationMode="server" - rowCount={rowCount ?? 999999999} + rowCount={rowCount} pageSizeOptions={(() => { const pageSizeOptions = [25, 50, 100]; diff --git a/web/src/ui/pages/dataExplorer/UrlInput.tsx b/web/src/ui/pages/dataExplorer/UrlInput.tsx index 67698c5f3..08e52b8f1 100644 --- a/web/src/ui/pages/dataExplorer/UrlInput.tsx +++ b/web/src/ui/pages/dataExplorer/UrlInput.tsx @@ -4,7 +4,7 @@ import { tss } from "tss"; import { getIconUrlByName } from "lazy-icons"; import { SearchBar } from "onyxia-ui/SearchBar"; import { useEffectOnValueChange } from "powerhooks/useEffectOnValueChange"; -import { declareComponentKeys } from "ui/i18n"; +import { declareComponentKeys, useTranslation } from "ui/i18n"; type Props = { className?: string; @@ -16,6 +16,7 @@ type Props = { export function UrlInput(props: Props) { const { className, url, onUrlChange, getIsValidUrl } = props; + const { t } = useTranslation({ UrlInput }); const [urlBeingTyped, setUrlBeingTyped] = useState(url); const isLoadable = urlBeingTyped !== url && getIsValidUrl(urlBeingTyped); @@ -51,10 +52,14 @@ export function UrlInput(props: Props) { ); @@ -76,5 +81,5 @@ const useStyles = tss } })); -const { i18n } = declareComponentKeys<"load">()({ UrlInput }); +const { i18n } = declareComponentKeys<"load" | "reset">()({ UrlInput }); export type I18n = typeof i18n; diff --git a/web/src/ui/pages/myFiles/MyFiles.tsx b/web/src/ui/pages/myFiles/MyFiles.tsx index c20d6a38f..18b4a5c9f 100644 --- a/web/src/ui/pages/myFiles/MyFiles.tsx +++ b/web/src/ui/pages/myFiles/MyFiles.tsx @@ -141,6 +141,7 @@ function MyFiles(props: Props) { ); const onOpenFile = useConstCallback(({ basename }) => { + //TODO use dataExplorer thunk if (basename.endsWith(".parquet") || basename.endsWith(".csv")) { const { path } = route.params;