From 8346fc4b3a8e4d96903b409edab97946896f734b Mon Sep 17 00:00:00 2001 From: "L. Jezak" <110393214+ukff@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:24:26 +0200 Subject: [PATCH] SM UI (#711) add service offerings support --- internal/api/api.go | 17 +- internal/api/vm/converters.go | 21 ++ internal/api/vm/vm.go | 12 + ui/src/App.css | 2 +- ui/src/components/SecretsView.tsx | 125 ++++--- ui/src/components/ServiceInstancesView.tsx | 29 +- ui/src/components/ServiceOfferingsView.tsx | 388 ++++++++++----------- ui/src/components/View.tsx | 84 ++--- ui/src/shared/api.tsx | 2 +- ui/src/shared/models.tsx | 15 +- ui/src/shared/validator.tsx | 26 ++ 11 files changed, 396 insertions(+), 325 deletions(-) create mode 100644 ui/src/shared/validator.tsx diff --git a/internal/api/api.go b/internal/api/api.go index 2f0c8a963..4cdff694b 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -32,6 +32,7 @@ func (a *API) Start() { mux.HandleFunc("GET /api/service-instance/{id}", a.GetServiceInstance) mux.HandleFunc("GET /api/service-offerings/{namespace}/{name}", a.ListServiceOfferings) mux.HandleFunc("GET /api/service-offering/{id}", a.GetServiceOffering) + go func() { err := http.ListenAndServe(":3006", mux) if err != nil { @@ -44,6 +45,17 @@ func (a *API) CreateServiceInstance(writer http.ResponseWriter, request *http.Re return } +func (a *API) GetServiceOffering(writer http.ResponseWriter, request *http.Request) { + a.setupCors(writer, request) + id := request.PathValue("id") + details, err := a.serviceManager.ServiceOfferingDetails(id) + if returnError(writer, err) { + return + } + response, err := json.Marshal(vm.ToServiceOfferingDetailsVM(details)) + returnResponse(writer, response, err) +} + func (a *API) ListServiceOfferings(writer http.ResponseWriter, request *http.Request) { a.setupCors(writer, request) namespace := request.PathValue("namespace") @@ -75,11 +87,6 @@ func (a *API) GetServiceInstance(writer http.ResponseWriter, request *http.Reque // not implemented in SM } -func (a *API) GetServiceOffering(writer http.ResponseWriter, request *http.Request) { - a.setupCors(writer, request) - // not implemented in SM -} - func (a *API) ListServiceInstances(writer http.ResponseWriter, request *http.Request) { a.setupCors(writer, request) // will be taken from SM diff --git a/internal/api/vm/converters.go b/internal/api/vm/converters.go index ae1747117..90b535b59 100644 --- a/internal/api/vm/converters.go +++ b/internal/api/vm/converters.go @@ -43,3 +43,24 @@ func ToServiceOfferingsVM(offerings *types.ServiceOfferings) ServiceOfferings { } return serviceOfferings } + +func ToServiceOfferingDetailsVM(serviceOfferings *types.ServiceOfferingDetails) ServiceOfferingDetails { + details := ServiceOfferingDetails{ + Plans: []ServiceOfferingPlan{}, + } + + for _, plan := range serviceOfferings.ServicePlans.ServicePlans { + details.LongDescription, _ = serviceOfferings.MetadataValueByFieldName(types.ServiceOfferingLongDescription) + supportUrl, _ := serviceOfferings.MetadataValueByFieldName(types.ServiceOfferingSupportURL) + documentationUrl, _ := serviceOfferings.MetadataValueByFieldName(types.ServiceOfferingDocumentationUrl) + planReturn := ServiceOfferingPlan{ + Name: plan.Name, + Description: plan.Description, + DocumentationUrl: documentationUrl, + SupportUrl: supportUrl, + } + details.Plans = append(details.Plans, planReturn) + } + + return details +} diff --git a/internal/api/vm/vm.go b/internal/api/vm/vm.go index 031a86fb6..3aa4d4288 100644 --- a/internal/api/vm/vm.go +++ b/internal/api/vm/vm.go @@ -35,3 +35,15 @@ type ServiceInstance struct { Name string `json:"name"` Namespace string `json:"namespace"` } + +type ServiceOfferingDetails struct { + LongDescription string `json:"longDescription"` + Plans []ServiceOfferingPlan `json:"plans"` +} + +type ServiceOfferingPlan struct { + Name string `json:"name"` + Description string `json:"description"` + DocumentationUrl string `json:"documentationUrl"` + SupportUrl string `json:"supportUrl"` +} diff --git a/ui/src/App.css b/ui/src/App.css index 969738504..1780dc194 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -43,5 +43,5 @@ html { body { margin: 0px auto; - width: 50vw; + width: 100vw; } \ No newline at end of file diff --git a/ui/src/components/SecretsView.tsx b/ui/src/components/SecretsView.tsx index 0bf734d80..af37c28a3 100644 --- a/ui/src/components/SecretsView.tsx +++ b/ui/src/components/SecretsView.tsx @@ -1,70 +1,91 @@ import * as ui5 from "@ui5/webcomponents-react"; import axios from "axios"; -import { useEffect, useState } from "react"; -import { Secrets } from "../shared/models"; +import {useEffect, useState} from "react"; +import {Secrets} from "../shared/models"; +import Ok from "../shared/validator"; import api from "../shared/api"; +import ServiceOfferingsView from "./ServiceOfferingsView"; function SecretsView(props: any) { - const [secrets, setSecrets] = useState(); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [secrets, setSecrets] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - useEffect(() => { - axios - .get(api("secrets")) - .then((response) => { - setSecrets(response.data); + useEffect(() => { + setLoading(true); + axios + .get(api("secrets")) + .then((response) => { + setLoading(false); + setSecrets(response.data); + if (Ok(response.data) && Ok(response.data.items)) { + const secret = formatSecretText(response.data.items[0].name, response.data.items[0].namespace) + props.handler(secret); + props.setPageContent(); + } else { + props.handler(formatSecretText("", "")); + } + }) + .catch((error) => { + setLoading(false); + setError(error); + setSecrets(undefined); + props.handler(formatSecretText("", "")); + }); setLoading(false); - props.handler( - formatDisplay( - response.data.items[0].name, - response.data.items[0].namespace - ) - ); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - }, []); + }, []); - if (loading) { - return - } + if (loading) { + return + } - if (error) { - return Error: {error}; - } + if (error) { + props.handler(formatSecretText("", "")); + return + } - const renderData = () => { - return secrets?.items.map((s, i) => { - return ( - {formatDisplay(s.name, s.namespace)} - ); - }); - }; + const renderData = () => { + // @ts-ignore + if (!Ok(secrets) || !Ok(secrets.items)){ + return
+ <> + {formatSecretText("", "")} + +
+ } + return secrets?.items.map((secret, index) => { + return ( + {formatSecretText(secret.name, secret.namespace)} + ); + }); + }; - return ( -
- <> + return (
- { - // @ts-ignore - props.handler(e.target.value); - }} - > - {renderData()} - + <> +
+ { + // @ts-ignore + const secret = e .target.value; + props.handler(secret); + props.setPageContent(); + }} + > + {renderData()} + +
+
- -
- ); + ); } -function formatDisplay(secretName: string, secretNamespace: string) { - return `${secretName} in (${secretNamespace})`; +function formatSecretText(secretName: string, secretNamespace: string) { + if (secretName === "" || secretNamespace === "") { + return "No secret found" + } + return `${secretName} in (${secretNamespace})`; } export default SecretsView; diff --git a/ui/src/components/ServiceInstancesView.tsx b/ui/src/components/ServiceInstancesView.tsx index 1a89c0f82..dc1d54ff1 100644 --- a/ui/src/components/ServiceInstancesView.tsx +++ b/ui/src/components/ServiceInstancesView.tsx @@ -4,6 +4,7 @@ import axios from "axios"; import { useEffect, useState, useRef } from "react"; import { createPortal } from "react-dom"; import api from "../shared/api"; +import Ok from "../shared/validator"; function ServiceInstancesView() { const [serviceInstances, setServiceInstances] = useState(); @@ -24,24 +25,29 @@ function ServiceInstancesView() { axios .get(api("service-instances")) .then((response) => { - setServiceInstances(response.data); - setLoading(false); + setLoading(false); + setServiceInstances(response.data); }) .catch((error) => { - setError(error); - setLoading(false); + setLoading(false); + setError(error); }); + setLoading(false) }, []); if (loading) { - return + return } if (error) { - return Error: {error}; + return } const renderData = () => { + // @ts-ignore + if (!Ok(serviceInstances) || !Ok(serviceInstances.items)) { + return + } return serviceInstances?.items.map((brief, index) => { return ( <> @@ -69,10 +75,6 @@ function ServiceInstancesView() { } onClick={handleOpen} - onLoadMore={function _a() {}} - onPopinChange={function _a() {}} - onRowClick={function _a() { }} - onSelectionChange={function _a() {}} > {renderData()} @@ -96,15 +98,8 @@ function ServiceInstancesView() { } headerText="Dialog Header" - onAfterClose={function _a() {}} - onAfterOpen={function _a() {}} - onBeforeClose={function _a() {}} - onBeforeOpen={function _a() {}} > - - List Item 1 - , document.body diff --git a/ui/src/components/ServiceOfferingsView.tsx b/ui/src/components/ServiceOfferingsView.tsx index 82a8b714a..2a9a66b3e 100644 --- a/ui/src/components/ServiceOfferingsView.tsx +++ b/ui/src/components/ServiceOfferingsView.tsx @@ -1,225 +1,211 @@ import * as ui5 from "@ui5/webcomponents-react"; -import { useEffect, useState, useRef } from "react"; -import { createPortal } from "react-dom"; - +import {useEffect, useRef, useState} from "react"; +import {createPortal} from "react-dom"; import axios from "axios"; -import { ServiceOfferingDetails, ServiceOfferings } from "../shared/models"; -import ts from "typescript"; +import {ServiceOfferingDetails, ServiceOfferings} from "../shared/models"; import api from "../shared/api"; +import "@ui5/webcomponents-icons/dist/AllIcons.js" +import "@ui5/webcomponents-fiori/dist/illustrations/NoEntries.js" +import "@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js" +import Ok from "../shared/validator"; function ServiceOfferingsView(props: any) { - const [offerings, setOfferings] = useState(); - const [serviceOfferingDetails, setServiceOfferingDetails] = - useState(); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [labels, setLabels] = useState(); - const dialogRef = useRef(null); - const handleOpen = (id: any) => { - // @ts-ignore - dialogRef.current.show(); - load(id); - }; - const handleClose = () => { - // @ts-ignore - dialogRef.current.close(); - }; - - useEffect(() => { - const splited = splitSecret(props.secret); - if (splited) { - setLoading(true); - axios - .get( - api( - `service-offerings/${splited.namespace}/${splited.secretName}` - ) - ) - .then((response) => { - setOfferings(response.data); - setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } - }, []); + const [offerings, setOfferings] = useState(); + const [serviceOfferingDetails, setServiceOfferingDetails] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const dialogRef = useRef(null); + + const handleOpen = (id: any) => { + // @ts-ignore + dialogRef.current.show(); + load(id); + }; + const handleClose = () => { + // @ts-ignore + dialogRef.current.close(); + }; + + useEffect(() => { + if (!Ok(props.secret)) { + return; + } + const secretText = splitSecret(props.secret); + if (Ok(secretText)) { + setLoading(true); + axios + .get( + api( + `service-offerings/${secretText.namespace}/${secretText.secretName}` + ) + ) + .then((response) => { + setLoading(false); + setOfferings(response.data); + }) + .catch((error) => { + setLoading(false); + setError(error); + }); + setLoading(false); + } + }, [props.secret]); - if (loading) { - return - } + if (loading) { + return + } - if (error) { - return Error: {error}; - } + if (error) { + return + } - function getImg(b64: string) { - if (b64 == null) { - return ""; - } else { - return b64; + function getImg(b64: string) { + if (!Ok(b64) || b64 === "not found") { + // grey color + return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="; + } else { + return b64; + } } - } - function load(id: string) { - setLoading(true); - axios - .get(api(`service-offering/${id}`)) - .then((response) => { - setServiceOfferingDetails(response.data); + function load(id: string) { + setLoading(true); + axios + .get(api(`service-offering/${id}`)) + .then((response) => { + setLoading(false); + setServiceOfferingDetails(response.data); + }) + .catch((error) => { + setLoading(false); + setError(error); + }); setLoading(false); - }) - .catch((error) => { - setError(error); - setLoading(false); - }); - } - - const renderData = () => { - return offerings?.items.map((offering, index) => { - return ( - <> - { - handleOpen(offering.id); - }} - header={ - - - - } - subtitleText={offering.metadata.displayName} - titleText={offering.catalogName} - status={formatStatus(index, offerings?.numItems)} - interactive - /> - } - > + } + + const renderData = () => { + // @ts-ignore + if (!Ok(offerings) || !Ok(offerings.items)) { + return + } + return offerings?.items.map((offering, index) => { + // @ts-ignore + return ( + <> + { + handleOpen(offering.id); + }} + header={ + + + + } + subtitleText={offering.description} + titleText={offering.catalogName} + status={formatStatus(index, offerings?.numItems)} + interactive + /> + } + > + - <> - {createPortal( - Close - } - /> - } - onAfterClose={function _a() {}} - onAfterOpen={function _a() {}} - onBeforeClose={function _a() {}} - onBeforeOpen={function _a() {}} - > - {serviceOfferingDetails?.longDescription} + <> + {createPortal( + Close + } + /> + } + > + + {serviceOfferingDetails?.longDescription} + + + + + + + { + serviceOfferingDetails?.plans.map((plan, index) => + ( + {plan.name} + + )) + } + + + + - - - - - - - - - - - - - - Option 1 - - - { - const it = ( - - - - - ); - // @ts-ignore - setLabels([...labels, it]); - }} - /> - {labels} - - - - - - - - - - - - Create - - - - - , - document.body - )} - - - ); - }); - }; + + + + + + + + + + + + + + , + document.body + )} + + + ); + }); + }; - return <>{renderData()}; + return <>{renderData()}; } function splitSecret(secret: string) { - if (secret == null) { - return {}; - } - const secretParts = secret.split(" "); - const secretName = secretParts[0]; - let namespace = secretParts[2].replace("(", ""); - namespace = namespace.replace(")", ""); - return { secretName, namespace }; + if (secret == null) { + return {}; + } + const secretParts = secret.split(" "); + const secretName = secretParts[0]; + let namespace = secretParts[2].replace("(", ""); + namespace = namespace.replace(")", ""); + return {secretName, namespace}; } function formatStatus(i: number, j: number) { - return `${++i} of ${j}`; + return `${++i} of ${j}`; } export default ServiceOfferingsView; diff --git a/ui/src/components/View.tsx b/ui/src/components/View.tsx index cb76c0dc8..41418d680 100644 --- a/ui/src/components/View.tsx +++ b/ui/src/components/View.tsx @@ -7,10 +7,10 @@ import React from "react"; function Overview(props: any) { const [secret, setSecret] = React.useState(null); const [pageContent, setPageContent] = React.useState(); - function handler(e: any) { - setSecret(e); + function handler(s: any) { + setSecret(s); } - + return ( <> SAP BTP, Kyma runtime} startContent={Select your credentials:} > - handler(e)} style={{ width: "100vw" }} /> + handler(e)} style={{ width: "100vw" }} + setPageContent={(e: any) => setPageContent(e)} + /> <> - - - { - setPageContent(); - }} - /> - { - setPageContent(); - }} - /> - - - {pageContent} - - +
+ + + { + setPageContent(); + }} + /> + { + setPageContent(); + }} + /> + + + {pageContent} + + +
+ ); diff --git a/ui/src/shared/api.tsx b/ui/src/shared/api.tsx index fb429e170..1ad6ad23c 100644 --- a/ui/src/shared/api.tsx +++ b/ui/src/shared/api.tsx @@ -2,4 +2,4 @@ function api(url :string) { return `http://localhost:3006/api/${url}` } -export default api ; \ No newline at end of file +export default api; \ No newline at end of file diff --git a/ui/src/shared/models.tsx b/ui/src/shared/models.tsx index 26a408aff..77b3d5d38 100644 --- a/ui/src/shared/models.tsx +++ b/ui/src/shared/models.tsx @@ -27,6 +27,14 @@ export interface ServiceOfferingMetadata { export interface ServiceOfferingDetails { longDescription: string; + plans: ServiceOfferingPlan[]; +} + +export interface ServiceOfferingPlan { + name: string; + description: string; + supportUrl: string; + documentationUrl: string; } export interface ServiceInstances { @@ -45,11 +53,4 @@ export interface ServiceInstanceBindings { id: string; name: string; namespace: string; -} - -export interface ServiceInstanceDetails { - id: string; - name: string; - context: string; - namespace: string; } \ No newline at end of file diff --git a/ui/src/shared/validator.tsx b/ui/src/shared/validator.tsx new file mode 100644 index 000000000..1e2e37087 --- /dev/null +++ b/ui/src/shared/validator.tsx @@ -0,0 +1,26 @@ +function Ok(value: any) { + if (value == null) { + return false + } + + if (value == undefined) { + return false + } + + if (!value) { + return false + } + + if ( typeof value == 'string' && value == "") { + return false + } + + if (Array.isArray(value)) { + if (value.length == 0) { + return false + } + } + return true +} + +export default Ok; \ No newline at end of file