diff --git a/webui/src/extendable/plugins/impls/DefaultCustomObjectRenderers.tsx b/webui/src/extendable/plugins/impls/DefaultCustomObjectRenderers.tsx new file mode 100644 index 00000000000..eefd2fbc810 --- /dev/null +++ b/webui/src/extendable/plugins/impls/DefaultCustomObjectRenderers.tsx @@ -0,0 +1,11 @@ +import { PluginCustomObjectRenderers } from "../pluginCustomObjectRenderers"; + +const DefaultCustomObjectRenderersPlugin: PluginCustomObjectRenderers = { + init: () => { + }, + get: () => { + return null; + } +} + +export default DefaultCustomObjectRenderersPlugin; diff --git a/webui/src/extendable/plugins/pluginCustomObjectRenderers.ts b/webui/src/extendable/plugins/pluginCustomObjectRenderers.ts new file mode 100644 index 00000000000..7f13d76e063 --- /dev/null +++ b/webui/src/extendable/plugins/pluginCustomObjectRenderers.ts @@ -0,0 +1,8 @@ +import React from "react"; +import { RendererComponent } from "../../pages/repositories/repository/fileRenderers/types"; +import { ConfigType } from "../../lib/hooks/configProvider"; + +export interface PluginCustomObjectRenderers { + init: (config?: ConfigType) => void; + get: (contentType?: string, fileExtension?: string) => React.FC | null; +} diff --git a/webui/src/extendable/plugins/pluginManager.ts b/webui/src/extendable/plugins/pluginManager.ts index e4fa9137e6c..37d084b250e 100644 --- a/webui/src/extendable/plugins/pluginManager.ts +++ b/webui/src/extendable/plugins/pluginManager.ts @@ -1,14 +1,25 @@ import { PluginRepoCreationForm } from "./pluginRepoCreationForm"; import DefaultRepoCreationFormPlugin from "./impls/DefaultRepoCreationFormPlugin"; +import { PluginCustomObjectRenderers } from "./pluginCustomObjectRenderers"; +import DefaultCustomObjectRenderersPlugin from "./impls/DefaultCustomObjectRenderers"; export class PluginManager { private _repoCreationForm: PluginRepoCreationForm = DefaultRepoCreationFormPlugin; + private _customObjectRenderers: PluginCustomObjectRenderers = DefaultCustomObjectRenderersPlugin; overridePluginRepoCreationForm(pluginRepoCreationForm: PluginRepoCreationForm): void { this._repoCreationForm = pluginRepoCreationForm; } - get repoCreationForm(): PluginRepoCreationForm | null { + get repoCreationForm(): PluginRepoCreationForm { return this._repoCreationForm; } + + overridePluginCustomObjectRenderers(pluginCustomObjectRenderers: PluginCustomObjectRenderers): void { + this._customObjectRenderers = pluginCustomObjectRenderers; + } + + get customObjectRenderers(): PluginCustomObjectRenderers { + return this._customObjectRenderers; + } } diff --git a/webui/src/lib/api/index.js b/webui/src/lib/api/index.js index 83b2c63968c..a6755fe23b6 100644 --- a/webui/src/lib/api/index.js +++ b/webui/src/lib/api/index.js @@ -814,6 +814,18 @@ class Objects { return response.text() } + async getRange(repoId, ref, path, start, end, presign = false) { + const query = qs({ path, presign }); + const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects?` + query, + {method: 'GET'}, + {'Range': `bytes=${start}-${end}`} + ); + if (response.status !== 200 && response.status !== 206) { + throw new Error(await extractError(response)); + } + return response.text(); + } + async head(repoId, ref, path) { const query = qs({path}); const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects?` + query, { @@ -878,7 +890,7 @@ class Commits { } async commit(repoId, branchId, message, metadata = {}) { - const response = await apiRequest(`/repositories/${repoId}/branches/${branchId}/commits`, { + const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/commits`, { method: 'POST', body: JSON.stringify({message, metadata}), }); @@ -1096,8 +1108,9 @@ class Config { case 200: { const cfg = await response.json(); const storages = buildStoragesConfigs(cfg); + const uiConfig = cfg['ui_config']; const versionConfig = cfg['version_config']; - return {storages, versionConfig}; + return {storages, uiConfig, versionConfig}; } case 409: throw new Error('Conflict'); diff --git a/webui/src/lib/hooks/configProvider.tsx b/webui/src/lib/hooks/configProvider.tsx index 77a5e070f75..357d60a9dd6 100644 --- a/webui/src/lib/hooks/configProvider.tsx +++ b/webui/src/lib/hooks/configProvider.tsx @@ -2,6 +2,7 @@ import React, { createContext, FC, useContext, useEffect, useState, } from "reac import { config } from "../api"; import useUser from "./user"; +import { usePluginManager } from "../../extendable/plugins/pluginsContext"; type ConfigContextType = { error: Error | null; @@ -10,11 +11,12 @@ type ConfigContextType = { }; type ConfigType = { - storages: StorageConfigType[] | null; + storages?: StorageConfig[]; + uiConfig?: UIConfig; versionConfig?: VersionConfig; -} +}; -type StorageConfigType = { +type StorageConfig = { blockstore_namespace_ValidityRegex: string | null; blockstore_namespace_example: string | null; blockstore_type: string | null; @@ -25,7 +27,19 @@ type StorageConfigType = { pre_sign_support_ui: boolean; }; +type UIConfig = { + custom_viewers?: Array; +}; + +type CustomViewer = { + name: string; + url: string; + content_types?: Array; + extensions?: Array; +}; + type VersionConfig = { + upgrade_recommended?: boolean; upgrade_url?: string; version_context?: string; version?: string; @@ -42,13 +56,16 @@ const configContext = createContext(configInitialState); const useConfigContext = () => useContext(configContext); const ConfigProvider: FC<{children: React.ReactNode}> = ({children}) => { + const pluginManager = usePluginManager(); const {user} = useUser(); const [storageConfig, setConfig] = useState(configInitialState); useEffect(() => { config.getConfig() - .then(configData => - setConfig({config: configData, loading: false, error: null})) + .then(configData => { + pluginManager.customObjectRenderers?.init(configData); + setConfig({config: configData, loading: false, error: null}); + }) .catch((error) => setConfig({config: null, loading: false, error})); }, [user]); @@ -60,4 +77,6 @@ const ConfigProvider: FC<{children: React.ReactNode}> = ({children}) => { ); }; -export { ConfigProvider, useConfigContext }; \ No newline at end of file +export type { ConfigType, CustomViewer }; + +export { ConfigProvider, useConfigContext }; diff --git a/webui/src/pages/repositories/repository/fileRenderers/index.tsx b/webui/src/pages/repositories/repository/fileRenderers/index.tsx index d2dda814f97..8d6f96e5cce 100644 --- a/webui/src/pages/repositories/repository/fileRenderers/index.tsx +++ b/webui/src/pages/repositories/repository/fileRenderers/index.tsx @@ -1,7 +1,7 @@ -import React, {FC} from "react"; +import React, { FC } from "react"; import SyntaxHighlighter from "react-syntax-highlighter"; -import {FileType, RendererComponent} from "./types"; -import {DuckDBRenderer} from "./data"; +import { FileType, RendererComponent } from "./types"; +import { DuckDBRenderer } from "./data"; import { GeoJSONRenderer, ImageRenderer, @@ -13,19 +13,20 @@ import { TextRenderer, UnsupportedFileType } from "./simple"; +import { usePluginManager } from "../../../../extendable/plugins/pluginsContext"; const MAX_FILE_SIZE = 20971520; // 20MiB -export const Renderers: {[fileType in FileType] : FC } = { +export const Renderers: { [fileType in FileType]: FC } = { [FileType.DATA]: props => ( ), [FileType.MARKDOWN]: props => ( - } /> + }/> ), [FileType.IPYNB]: props => ( @@ -52,11 +53,11 @@ export const Renderers: {[fileType in FileType] : FC } = { [FileType.GEOJSON]: props => ( - } /> + }/> ), } -export const guessLanguage = (extension: string | null, contentType: string | null) => { +export const guessLanguage = (extension?: string, contentType?: string) => { switch (extension) { case 'py': extension = 'python' @@ -109,7 +110,7 @@ export const guessLanguage = (extension: string | null, contentType: string | n } -export function guessType(contentType: string | null, fileExtension: string | null): FileType { +export function guessType(contentType?: string, fileExtension?: string): FileType { switch (contentType) { case 'application/x-yaml': case 'application/yaml': @@ -178,7 +179,14 @@ export function guessType(contentType: string | null, fileExtension: string | nu } export const ObjectRenderer: FC = (props: RendererComponent) => { - const fileType = guessType(props.contentType, props.fileExtension) + const pluginManager = usePluginManager(); + + const customRenderer = pluginManager.customObjectRenderers.get(props.contentType, props.fileExtension) + if (customRenderer) { + return customRenderer(props) + } + + const fileType = guessType(props.contentType, props.fileExtension) if (fileType !== FileType.DATA && props.sizeBytes > MAX_FILE_SIZE) return Renderers[FileType.TOO_LARGE](props) return Renderers[fileType](props) diff --git a/webui/src/pages/repositories/repository/fileRenderers/types.tsx b/webui/src/pages/repositories/repository/fileRenderers/types.tsx index bd386c16e4d..f79b925f153 100644 --- a/webui/src/pages/repositories/repository/fileRenderers/types.tsx +++ b/webui/src/pages/repositories/repository/fileRenderers/types.tsx @@ -2,8 +2,8 @@ export interface RendererComponent { repoId: string; refId: string; path: string; - fileExtension: string | null; - contentType: string | null; + fileExtension?: string; + contentType?: string; sizeBytes: number; presign?: boolean; } diff --git a/webui/src/pages/repositories/repository/objectViewer.tsx b/webui/src/pages/repositories/repository/objectViewer.tsx index 72206506ee0..301d5f89d9b 100644 --- a/webui/src/pages/repositories/repository/objectViewer.tsx +++ b/webui/src/pages/repositories/repository/objectViewer.tsx @@ -34,7 +34,7 @@ interface FileContentsProps { path: string; loading: boolean; error: Error | null; - contentType?: string | null; + contentType?: string; fileExtension: string; sizeBytes: number; showFullNavigator?: boolean; @@ -50,10 +50,10 @@ export const getFileExtension = (objectName: string): string => { return objectNameParts[objectNameParts.length - 1]; }; -export const getContentType = (headers: Headers): string | null => { - if (!headers) return null; +export const getContentType = (headers: Headers): string | undefined => { + if (!headers) return undefined; - return headers.get("Content-Type") ?? null; + return headers.get("Content-Type") ?? undefined; }; const FileObjectsViewerPage = () => { @@ -116,7 +116,7 @@ export const FileContents: FC = ({ path, loading, error, - contentType = null, + contentType = undefined, fileExtension = "", sizeBytes = -1, showFullNavigator = true,