From 05bd790ca3115c274fd4d6dab7f54deb6dd56649 Mon Sep 17 00:00:00 2001 From: Ricardo M Date: Mon, 24 Apr 2023 14:33:09 +0200 Subject: [PATCH 1/2] feat(api): Allow configuring the API URL at runtime Currently, the API URL is retrieved using an environment variable available at build time only. This creates friction in the VSCode extension context as it blocks running more than one instance of it. This commit adds a constructor parameter to optionally receive the API URL so it can be overridden through the VSCode bootstrap process. The API URL still defaults to the same environment variable provided at build time. fixes: [1451](https://github.com/KaotoIO/kaoto-ui/issues/1451) --- src/api/apiService.ts | 28 ++--- src/api/index.ts | 2 +- src/api/request.ts | 91 ---------------- src/api/requestService.ts | 106 +++++++++++++++++++ src/kogito-integration/KaotoEditorFactory.ts | 5 +- src/kogito-integration/KaotoEditorView.tsx | 34 +++--- 6 files changed, 145 insertions(+), 121 deletions(-) delete mode 100644 src/api/request.ts create mode 100644 src/api/requestService.ts diff --git a/src/api/apiService.ts b/src/api/apiService.ts index f9464c2e9..89514c2ad 100644 --- a/src/api/apiService.ts +++ b/src/api/apiService.ts @@ -1,4 +1,4 @@ -import request from './request'; +import { RequestService } from './requestService'; import { IIntegration, IStepProps } from '@kaoto/types'; const apiVersion = '/v1'; @@ -10,7 +10,7 @@ const apiVersion = '/v1'; */ export async function fetchCapabilities(namespace?: string) { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/capabilities`, contentType: 'application/json', queryParams: { @@ -26,7 +26,7 @@ export async function fetchCapabilities(namespace?: string) { export async function fetchDefaultNamespace() { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/capabilities/namespace`, contentType: 'application/json', }); @@ -54,7 +54,7 @@ export async function fetchCatalogSteps( cache?: RequestCache | undefined ) { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/steps`, cache, queryParams: { @@ -76,7 +76,7 @@ export async function fetchCatalogSteps( */ export async function fetchCompatibleDSLs(props: { namespace?: string; steps: IStepProps[] }) { try { - const resp = await request.post({ + const resp = await RequestService.post({ endpoint: `${apiVersion}/integrations/dsls`, contentType: 'application/json', body: props.steps, @@ -98,7 +98,7 @@ export async function fetchCompatibleDSLs(props: { namespace?: string; steps: IS */ export async function fetchDeployment(name: string, namespace?: string): Promise { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/deployment/${name}`, contentType: 'application/json', queryParams: { @@ -119,7 +119,7 @@ export async function fetchDeployment(name: string, namespace?: string): Promise */ export async function fetchDeployments(cache?: RequestCache | undefined, namespace?: string) { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/deployments`, contentType: 'application/json', cache, @@ -146,7 +146,7 @@ export async function fetchDeploymentLogs( lines?: number ): Promise { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/deployments/${name}/logs`, contentType: 'application/json', queryParams: { @@ -177,7 +177,7 @@ export async function fetchIntegrationJson( namespace?: string ) { try { - const resp = await request.post({ + const resp = await RequestService.post({ endpoint: `${apiVersion}/integrations`, contentType: typeof data === 'string' ? 'text/yaml' : 'application/json', body: typeof data === 'string' ? data : { steps: data }, @@ -204,7 +204,7 @@ export async function fetchIntegrationJson( */ export async function fetchIntegrationSourceCode(newIntegration: IIntegration, namespace?: string) { try { - const resp = await request.post({ + const resp = await RequestService.post({ endpoint: `${apiVersion}/integrations?dsl=${newIntegration.dsl}`, contentType: 'application/json', body: newIntegration, @@ -221,7 +221,7 @@ export async function fetchIntegrationSourceCode(newIntegration: IIntegration, n export async function fetchStepDetails(id?: string, namespace?: string) { try { - const resp = await request.get({ + const resp = await RequestService.get({ endpoint: `${apiVersion}/steps/id/${id}`, queryParams: { namespace: namespace ?? 'default', @@ -245,7 +245,7 @@ export async function fetchStepDetails(id?: string, namespace?: string) { */ export async function fetchViews(data: IStepProps[], namespace?: string) { try { - const resp = await request.post({ + const resp = await RequestService.post({ endpoint: `${apiVersion}/view-definitions`, contentType: 'application/json', body: data, @@ -270,7 +270,7 @@ export async function fetchViews(data: IStepProps[], namespace?: string) { export async function startDeployment(integrationSource: string, name: string, namespace?: string) { try { const params = namespace ? `?namespace=${namespace}` : ''; - const resp = await request.post({ + const resp = await RequestService.post({ endpoint: `${apiVersion}/deployments/${name.toLowerCase()}${params}`, contentType: 'text/yaml', body: integrationSource, @@ -289,7 +289,7 @@ export async function startDeployment(integrationSource: string, name: string, n */ export async function stopDeployment(deploymentName: string, namespace?: string) { try { - const resp = await request.delete({ + const resp = await RequestService.delete({ endpoint: `${apiVersion}/deployments/${deploymentName.toLowerCase()}`, contentType: 'application/json', queryParams: { namespace: namespace ?? 'default' }, diff --git a/src/api/index.ts b/src/api/index.ts index 9014f3901..a223bf854 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,2 +1,2 @@ export * from './apiService'; -export * from './request'; +export * from './requestService'; diff --git a/src/api/request.ts b/src/api/request.ts deleted file mode 100644 index 27dfa28c2..000000000 --- a/src/api/request.ts +++ /dev/null @@ -1,91 +0,0 @@ -export type FetchMethod = 'GET' | 'PATCH' | 'PUT' | 'POST' | 'DELETE'; - -export interface IFetchHeaders { - [s: string]: string; -} - -export interface IFetch { - endpoint: string; - method?: FetchMethod; - headers?: IFetchHeaders; - body?: any; - - /** - * Default: 'application/json' - */ - accept?: string; - - cache?: RequestCache | undefined; - - /** - * Default: 'application/json' - */ - contentType?: string; - - // Object of key/value string for passing query parameters - queryParams?: { - [name: string]: string | undefined | null | number | boolean; - }; - - /** - * Whether to stringify the data to JSON, overrides the content type - */ - stringifyBody?: boolean; -} -let apiURL = process.env.KAOTO_API; - -// converts an object into a query string -// ex: {type : 'Kamelet'} -> &type=Kamelet -function objectToQueryString(obj: { [x: string]: string | undefined | null | number | boolean }) { - return Object.keys(obj) - .map((key) => key + '=' + obj[key]) - .join('&'); -} - -const api = ({ - endpoint, - method, - headers = {}, - body, - cache, - contentType, - accept, - queryParams, - stringifyBody = true, -}: IFetch) => { - headers = { ...headers }; - - let options: RequestInit = { - method, - body: contentType?.includes('application/json') && stringifyBody ? JSON.stringify(body) : body, - cache: cache ?? 'default', - /** - * TODO: Omit for now, reassess for prod - */ - credentials: 'omit', - headers: { - Accept: accept ?? 'application/json,text/plain,text/yaml,*/*', - 'Content-Type': contentType ?? 'application/json', - ...headers, - }, - mode: 'cors', - redirect: 'follow', - referrerPolicy: 'no-referrer', - }; - - // if params exists and method is GET, add query string to url - // otherwise, add query params as a "body" property to the options object - if (queryParams && method === 'GET') { - endpoint += '?' + objectToQueryString(queryParams); - } - - return fetch(`${apiURL}${endpoint}`, options); -}; - -export default { - get: (options: IFetch) => api({ method: 'GET', ...options }), - post: (options: IFetch) => api({ method: 'POST', ...options }), - put: (options: IFetch) => api({ method: 'PUT', ...options }), - // patch: (options: IFetch) => api({ method: 'PATCH', ...options }), - delete: (options: IFetch) => api({ method: 'DELETE', ...options }), -}; diff --git a/src/api/requestService.ts b/src/api/requestService.ts new file mode 100644 index 000000000..d8d52a83e --- /dev/null +++ b/src/api/requestService.ts @@ -0,0 +1,106 @@ +export type FetchMethod = 'GET' | 'PATCH' | 'PUT' | 'POST' | 'DELETE'; + +export interface IFetchHeaders { + [s: string]: string; +} + +export interface IFetch { + endpoint: string; + method?: FetchMethod; + headers?: IFetchHeaders; + body?: any; + + /** + * Default: 'application/json' + */ + accept?: string; + + cache?: RequestCache | undefined; + + /** + * Default: 'application/json' + */ + contentType?: string; + + // Object of key/value string for passing query parameters + queryParams?: { + [name: string]: string | undefined | null | number | boolean; + }; + + /** + * Whether to stringify the data to JSON, overrides the content type + */ + stringifyBody?: boolean; +} + +export class RequestService { + private static apiURL = process.env.KAOTO_API; + + static setApiURL(apiUrl: string): void { + this.apiURL = apiUrl; + } + + static async get(options: IFetch) { + return this.request({ method: 'GET', ...options }); + } + + static async post(options: IFetch) { + return this.request({ method: 'POST', ...options }); + } + + static async patch(options: IFetch) { + return this.request({ method: 'PATCH', ...options }); + } + + static async delete(options: IFetch) { + return this.request({ method: 'DELETE', ...options }); + } + + private static async request({ + endpoint, + method, + headers = {}, + body, + cache, + contentType, + accept, + queryParams, + stringifyBody = true, + }: IFetch): Promise { + headers = { ...headers }; + + let options: RequestInit = { + method, + body: contentType?.includes('application/json') && stringifyBody ? JSON.stringify(body) : body, + cache: cache ?? 'default', + /** + * TODO: Omit for now, reassess for prod + */ + credentials: 'omit', + headers: { + Accept: accept ?? 'application/json,text/plain,text/yaml,*/*', + 'Content-Type': contentType ?? 'application/json', + ...headers, + }, + mode: 'cors', + redirect: 'follow', + referrerPolicy: 'no-referrer', + }; + + // if params exists and method is GET, add query string to url + // otherwise, add query params as a "body" property to the options object + if (queryParams && method === 'GET') { + endpoint += '?' + this.objectToQueryString(queryParams); + } + + return fetch(`${this.apiURL}${endpoint}`, options); + }; + + // converts an object into a query string + // ex: {type : 'Kamelet'} -> &type=Kamelet + private static objectToQueryString(obj: { [x: string]: string | undefined | null | number | boolean }) { + return Object.keys(obj) + .map((key) => key + '=' + obj[key]) + .join('&'); + } +} diff --git a/src/kogito-integration/KaotoEditorFactory.ts b/src/kogito-integration/KaotoEditorFactory.ts index 6500b4b6d..6dd16182a 100644 --- a/src/kogito-integration/KaotoEditorFactory.ts +++ b/src/kogito-integration/KaotoEditorFactory.ts @@ -8,10 +8,13 @@ import { import { KaotoEditorView } from "./KaotoEditorView"; export class KaotoEditorFactory implements EditorFactory { + + constructor(private readonly apiUrl?: string) {} + public createEditor( envelopeContext: KogitoEditorEnvelopeContextType, initArgs: EditorInitArgs ): Promise { - return Promise.resolve(new KaotoEditorView(envelopeContext, initArgs)); + return Promise.resolve(new KaotoEditorView(envelopeContext, initArgs, this.apiUrl)); } } diff --git a/src/kogito-integration/KaotoEditorView.tsx b/src/kogito-integration/KaotoEditorView.tsx index b7c9bb2d4..1ef576293 100644 --- a/src/kogito-integration/KaotoEditorView.tsx +++ b/src/kogito-integration/KaotoEditorView.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -//@ts-nocheck import { RefObject, createRef } from "react"; import { Editor, + EditorApi, EditorInitArgs, EditorTheme, KogitoEditorChannelApi, @@ -27,37 +27,43 @@ import { import { DEFAULT_RECT } from "@kie-tools-core/guided-tour/dist/api"; import { Notification } from "@kie-tools-core/notifications/dist/api"; import { KaotoEditor } from "./KaotoEditor"; +import { RequestService } from '../api'; export class KaotoEditorView implements Editor { private readonly editorRef: RefObject; - public af_isReact = true; - public af_componentId: "kaoto-editor"; - public af_componentTitle: "Kaoto Editor"; + af_isReact = true; + af_componentId = "kaoto-editor"; + af_componentTitle = "Kaoto Editor"; constructor( private readonly envelopeContext: KogitoEditorEnvelopeContextType, - private readonly initArgs: EditorInitArgs + private readonly initArgs: EditorInitArgs, + private readonly apiUrl?: string, ) { this.editorRef = createRef(); + + if (this.apiUrl) { + RequestService.setApiURL(this.apiUrl); + } } - public async getElementPosition() { + async getElementPosition() { return DEFAULT_RECT; } - public setContent(path: string, content: string): Promise { + setContent(path: string, content: string): Promise { return this.editorRef.current!.setContent(path, content); } - public getContent(): Promise { + getContent(): Promise { return this.editorRef.current!.getContent(); } - public getPreview(): Promise { + getPreview(): Promise { return this.editorRef.current!.getPreview(); } - public af_componentRoot() { + af_componentRoot() { return ( { + async undo(): Promise { return this.editorRef.current!.undo(); } - public async redo(): Promise { + async redo(): Promise { return this.editorRef.current!.redo(); } - public async validate(): Promise { + async validate(): Promise { return this.editorRef.current!.validate(); } - public async setTheme(theme: EditorTheme) { + async setTheme(theme: EditorTheme) { return this.editorRef.current!.setTheme(theme); } } From f9ee892f6a2ded17047ad8d8e4f33fef7fd1c6a8 Mon Sep 17 00:00:00 2001 From: Ricardo M Date: Mon, 24 Apr 2023 15:09:42 +0200 Subject: [PATCH 2/2] chore: handle sonarcloud issues --- src/api/requestService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/requestService.ts b/src/api/requestService.ts index d8d52a83e..ce4c24c4d 100644 --- a/src/api/requestService.ts +++ b/src/api/requestService.ts @@ -15,7 +15,7 @@ export interface IFetch { */ accept?: string; - cache?: RequestCache | undefined; + cache?: RequestCache; /** * Default: 'application/json'