diff --git a/package.json b/package.json index 97d97d4..4df73d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sdlb_configuration_visualizer", - "version": "0.11.0", + "version": "0.11.1", "private": true, "type": "module", "dependencies": { diff --git a/src/api/fetchAPI_rest.ts b/src/api/fetchAPI_rest.ts index f818ce5..50897fe 100644 --- a/src/api/fetchAPI_rest.ts +++ b/src/api/fetchAPI_rest.ts @@ -2,6 +2,7 @@ import { Auth } from "aws-amplify"; import { fetchAPI } from "./fetchAPI"; import { ConfigData } from "../util/ConfigExplorer/ConfigData"; import { TstampEntry } from "../types"; +import { compareFunc, dateFromNumber, sortIfArray } from "../util/helpers"; export class fetchAPI_rest implements fetchAPI { url: string; @@ -17,7 +18,8 @@ export class fetchAPI_rest implements fetchAPI { private async fetch(url: string, init: Promise = this.getRequestInfo()) { const response = await fetch(url, await init); if (!response.ok) { - const msg = (await response.json())['message'] + ` (${response.status})`; + const json = await response.json() + const msg = (json['message'] || json['detail']) + ` (${response.status})`; throw new Error(msg); } return await response.json(); @@ -78,7 +80,7 @@ export class fetchAPI_rest implements fetchAPI { }; getConfigVersions = async (tenant: string, repo: string, env: string): Promise => { - return this.fetch(`${this.url}/versions?tenant=${tenant}&repo=${repo}&env=${env}`) + return this.fetch(`${this.url}/versions?tenant=${tenant}&repo=${repo}&env=${env}`).then(x => sortIfArray(x).reverse()) } getDescription = async ( @@ -90,12 +92,9 @@ export class fetchAPI_rest implements fetchAPI { version: string | undefined, ) => { const filename = `${elementType}/${elementName}.md`; - const response = await this.getDescriptionFile(filename, tenant, repo, env, version); - const responseBody = await response.json(); - if (!response.ok) { - throw new Error(responseBody["detail"]); - } - return await this.resolveDescriptionImageUrls(responseBody.content, tenant, repo, env, version); + const description = await this.getDescriptionFile(filename, tenant, repo, env, version); + if (description) return this.resolveDescriptionImageUrls((await description!.json()).content, tenant, repo, env, version); + else return Promise.resolve(undefined); }; /** @@ -130,12 +129,11 @@ export class fetchAPI_rest implements fetchAPI { // Last capture group: ")" const filename = match[2]; const promise = this.getDescriptionFile(filename, tenant, repo, env, version).then((response) => { - if (!response.ok) { - return response.json().then((error) => { - throw new Error(error["detail"]); - }); + if (response!.ok) { + return response!.blob().then((blob) => fileUrlMap.set(filename, URL.createObjectURL(blob))); + } else { + return Promise.resolve(undefined); } - return response.blob().then((blob) => fileUrlMap.set(filename, URL.createObjectURL(blob))); }); promises.push(promise); @@ -155,19 +153,29 @@ export class fetchAPI_rest implements fetchAPI { repo: string, env: string, version: string | undefined - ): Promise => { - return this.fetch( + ): Promise => { + const response = await fetch( `${this.url}/descriptions/${filename}?tenant=${tenant}&repo=${repo}&env=${env}&version=${version}`, - this.getRequestInfo("GET", { Accept: "image/*,*/*;q=0.8" }) + await this.getRequestInfo("GET", { Accept: "image/*,*/*;q=0.8" }) ); + if (response.status == 404) { + console.log(`getDescriptionFile ${filename} not found.`); + return Promise.resolve(undefined); + } + if (!response.ok) { + const json = await response.json() + const msg = (json['message'] || json['detail']) + ` (${response.status})`; + throw new Error(msg); + } + return response; }; getTstampEntries = async (type: string, subtype: string, elementName: string, tenant: string, repo: string, env: string): Promise => { return this.fetch(`${this.url}/dataobject/${subtype}/${elementName}/tstamps?tenant=${tenant}&repo=${repo}&env=${env}`) - .then((parsedJson) => - parsedJson.map( + .then((parsedJson: []) => + parsedJson.toSorted().reverse().map( (ts: number) => - ({ key: `${elementName}.${subtype}.${ts}`, elementName: elementName, tstamp: new Date(ts) } as TstampEntry) + ({ key: `${elementName}.${subtype}.${ts}`, elementName: elementName, ts, tstamp: dateFromNumber(ts) } as TstampEntry) ) ) .catch((error) => { @@ -178,7 +186,7 @@ export class fetchAPI_rest implements fetchAPI { getSchema = async (schemaTstampEntry: TstampEntry | undefined, tenant: string, repo: string, env: string) => { if (!schemaTstampEntry?.elementName || !schemaTstampEntry?.tstamp) return Promise.resolve(undefined); - return this.fetch(`${this.url}/dataobject/schema/${schemaTstampEntry!.elementName}?tenant=${tenant}&repo=${repo}&env=${env}&tstamp=${schemaTstampEntry.tstamp.getTime()}`) + return this.fetch(`${this.url}/dataobject/schema/${schemaTstampEntry!.elementName}?tenant=${tenant}&repo=${repo}&env=${env}&tstamp=${schemaTstampEntry.ts}`) .catch((error) => { console.log(error); return undefined; @@ -187,7 +195,7 @@ export class fetchAPI_rest implements fetchAPI { getStats = async (statsTstampEntry: TstampEntry | undefined, tenant: string, repo: string, env: string) => { if (!statsTstampEntry?.elementName || !statsTstampEntry?.tstamp) return Promise.resolve(undefined); - return this.fetch(`${this.url}/dataobject/stats/${statsTstampEntry!.elementName}?tenant=${tenant}&repo=${repo}&env=${env}&tstamp=${statsTstampEntry.tstamp.getTime()}`) + return this.fetch(`${this.url}/dataobject/stats/${statsTstampEntry!.elementName}?tenant=${tenant}&repo=${repo}&env=${env}&tstamp=${statsTstampEntry.ts}`) .then((parsedJson) => parsedJson.stats) .catch((error) => { console.log(error); @@ -215,15 +223,15 @@ export class fetchAPI_rest implements fetchAPI { }; getTenants = async () => { - return this.fetch(`${this.url}/tenants`); + return this.fetch(`${this.url}/tenants`).then(x => sortIfArray(x)); } getRepos = async (tenant: string) => { - return this.fetch(`${this.url}/repo?tenant=${tenant}`) + return this.fetch(`${this.url}/repo?tenant=${tenant}`).then(x => sortIfArray(x)) } getEnvs = async (tenant: string, repo: string) => { - return this.fetch(`${this.url}/envs?tenant=${tenant}&repo=${repo}`) + return this.fetch(`${this.url}/envs?tenant=${tenant}&repo=${repo}`).then(x => sortIfArray(x)) } getLicenses = async (tenant: string): Promise => { diff --git a/src/components/ConfigExplorer/ElementDetails.tsx b/src/components/ConfigExplorer/ElementDetails.tsx index e491772..458264c 100644 --- a/src/components/ConfigExplorer/ElementDetails.tsx +++ b/src/components/ConfigExplorer/ElementDetails.tsx @@ -78,6 +78,8 @@ export default function ElementDetails(props: { elementType: elementType as string })); + const hasSchema = schemaEntries && schemaEntries.length > 0 + return ( <> @@ -90,9 +92,9 @@ export default function ElementDetails(props: { Description - + - {elementType === "dataObjects" && Schema} + {elementType === "dataObjects" && Schema} diff --git a/src/layouts/RootLayoutSpinner.tsx b/src/layouts/RootLayoutSpinner.tsx index 0e0f5cc..79e6000 100644 --- a/src/layouts/RootLayoutSpinner.tsx +++ b/src/layouts/RootLayoutSpinner.tsx @@ -4,6 +4,9 @@ import { useFetchEnvs, useFetchRepos, useFetchTenants } from "../hooks/useFetchD import { useManifest } from "../hooks/useManifest"; import { useWorkspace } from "../hooks/useWorkspace"; import PageHeader from "./PageHeader"; +import MarkdownComponent from "../components/ConfigExplorer/MarkdownComponent"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; function WorkspaceSpinner({ children }) { @@ -23,19 +26,55 @@ function WorkspaceSpinner({ children }) { export function WorkspaceEmpty() { const { tenant } = useWorkspace(); + const manifest = useManifest(); + const url = manifest.data?.backendConfig.split(";")[1]; + const clientId = manifest.data?.auth["aws_user_pools_web_client_id"]; + + const markdown = ` +**Tenant '${tenant}' seems still empty.** + +Use the following steps based on our [getting-started](https://github.com/smart-data-lake/getting-started) guide to upload an SDLB configuration and runtime informations, +or select another tenant in the upper right corner. + +#### 1. Add global.uiBackend configuration +Adapt _repo_ (repository name) and _env_ (environment name) to your needs and use [Secret Providers](https://smartdatalake.ch/docs/reference/hoconSecrets) to hide the _password_ of your UI user. + + global { + + ... + + uiBackend { + baseUrl = "${url}" + tenant = ${tenant} + repo = getting-started + env = dev + authMode { + type = AWSUserPwdAuthMode + region = eu-central-1 + userPool = sdlb-ui + clientId = ${clientId} + useIdToken = true + user = "###ENV#user###" + password = "###ENV#pwd###" + } + } + } + +#### 2. Export SDLB configuration to the UI +Use the _exportConfigSchemaStats.sh_ script from the [getting-started](https://github.com/smart-data-lake/getting-started) folder to export the SDLB configuration to the UI. +This will initialize and populate the _repository_ and _environment_ in the UI, so it is no longer empty. + +#### 3. Run SDLB Job +Use the _startJob.sh_ script from the [getting-started](https://github.com/smart-data-lake/getting-started) folder to run an SDLB Job. +The SDLB Job will automatically pickup the global.uiBackend configuration and push its runtime information to the UI. + +` + + return <> - - - - Tenant '{tenant}' seems still empty.
- Select another tenant in the upper right corner,
- or use the following steps to upload an SDLB configuration and SDLB run states: -
    -
  1. configure global.uiBackend TODO
  2. -
  3. export configuration using TODO
  4. -
  5. run SDLB
  6. -
-
+ + + ; } diff --git a/src/types.ts b/src/types.ts index e395e6c..7228ac0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -204,6 +204,7 @@ export class Row implements MetaDataBaseObject { export interface TstampEntry { key: string; elementName: string; + ts: number; tstamp: Date; } diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 996a774..b2f94ff 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -68,10 +68,10 @@ export function onlyUnique(value, index, array) { * This can be used for sorting arrays. * usage: arr.sort(compareFunc("x")) */ -export function compareFunc(attr: any) { +export function compareFunc(attr: any, reverse: boolean = false) { return (a, b) => { if (a[attr] === b[attr]) return 0; - else return a[attr] > b[attr] || a[attr] === undefined ? 1 : -1; + else return (a[attr] > b[attr] || a[attr] === undefined ? 1 : -1) * (reverse ? -1 : 1); } } @@ -220,4 +220,14 @@ export function setAttributeFromPath ( entity, path, value) { } } }); -}; \ No newline at end of file +}; + +export function dateFromNumber( ts: number ) { + // convert number to date handling seconds or milliseconds format. + return new Date((ts<9000000000 ? ts*1000 : ts)) +} + +export function sortIfArray(v: TInput) { + if (Array.isArray(v)) return (v as []).sort() as TInput; + else v; +} \ No newline at end of file