gui/frontend/src/modules/mrs/dialogs/MrsServiceDialog.tsx (300 lines of code) (raw):

/* * Copyright (c) 2021, 2025, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, as * designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ import { DialogResponseClosure, IDialogRequest, IDictionary } from "../../../app-logic/general-types.js"; import type { IMrsAuthAppData } from "../../../communication/ProtocolMrs.js"; import { AwaitableValueEditDialog } from "../../../components/Dialogs/AwaitableValueEditDialog.js"; import { CommonDialogValueOption, IDialogSection, IDialogValidations, IDialogValues, type ICheckListDialogValue, } from "../../../components/Dialogs/ValueEditDialog.js"; import { CheckState, type ICheckboxProperties } from "../../../components/ui/Checkbox/Checkbox.js"; export interface IMrsServiceDialogData extends IDictionary { servicePath: string; name: string; comments: string; isCurrent: boolean; enabled: boolean; published: boolean; protocols: string[]; options: string; authPath: string; authCompletedUrlValidation: string; authCompletedUrl: string; authCompletedPageContent: string; metadata: string; linkedAuthAppIds: string[]; } export class MrsServiceDialog extends AwaitableValueEditDialog { protected override get id(): string { return "mrsServiceDialog"; } public override async show(request: IDialogRequest): Promise<IDictionary | DialogResponseClosure> { const dialogValues = this.dialogValues(request); const result = await this.doShow(() => { return dialogValues; }, { title: "MySQL REST Service" }); if (result.closure === DialogResponseClosure.Accept) { return this.processResults(result.values); } return DialogResponseClosure.Cancel; } protected override validateInput = (closing: boolean, values: IDialogValues): IDialogValidations => { const result: IDialogValidations = { messages: {}, requiredContexts: [], }; if (closing) { const mainSection = values.sections.get("mainSection"); if (mainSection) { const servicePath = mainSection.values.servicePath.value as string; if (!servicePath) { result.messages.servicePath = "The service path must not be empty."; } else if (!servicePath.startsWith("/")) { result.messages.servicePath = "The request path must start with /."; } else if (servicePath.toLowerCase() === "/mrs") { result.messages.servicePath = `The request path \`${servicePath}\` is reserved and cannot be used.`; } const name = mainSection.values.name.value as string; if (!name) { result.messages.name = "The service name must not be empty."; } } const optionsSection = values.sections.get("optionsSection"); if (optionsSection) { if (optionsSection.values.options.value) { try { JSON.parse(optionsSection.values.options.value as string); } catch (e) { result.messages.options = "Please provide a valid JSON object."; } } if (optionsSection.values.metadata.value) { try { JSON.parse(optionsSection.values.metadata.value as string); } catch (e) { result.messages.metadata = "Please provide a valid JSON object."; } } } } return result; }; private dialogValues(request: IDialogRequest): IDialogValues { const mainSection: IDialogSection = { caption: request.title, values: { servicePath: { type: "text", caption: "REST Service Path", value: request.values?.servicePath as string, horizontalSpan: 3, options: [CommonDialogValueOption.AutoFocus], description: "The URL context root of this service, has to start with / and needs to be unique.", }, name: { type: "text", caption: "REST Service Name", value: request.values?.name as string, horizontalSpan: 3, description: "The descriptive name of the REST service.", }, makeDefaultTitle: { type: "description", caption: "REST Service Flags", horizontalSpan: 2, options: [ CommonDialogValueOption.Grouped, CommonDialogValueOption.NewGroup, ], }, enabled: { type: "boolean", caption: "Enabled", value: (request.values?.enabled ?? true) as boolean, horizontalSpan: 2, options: [ CommonDialogValueOption.Grouped, ], }, makeDefault: { type: "boolean", caption: "Default", value: (request.values?.isCurrent ?? true) as boolean, horizontalSpan: 2, options: [ CommonDialogValueOption.Grouped, ], }, published: { type: "boolean", caption: "Published", value: (request.values?.published ?? false) as boolean, horizontalSpan: 2, options: [ CommonDialogValueOption.Grouped, ], }, }, }; const settingsSection: IDialogSection = { caption: "Settings", groupName: "group1", values: {}, }; request.parameters ??= {}; const authApps = request.parameters.authApps as IMrsAuthAppData[] ?? []; const linkedAuthApps = request.parameters.linkedAuthApps as IMrsAuthAppData[] ?? []; settingsSection.values.linkedAuthApps = { type: "checkList", caption: "Linked REST Authentication Apps", checkList: Object.values(authApps).map((app) => { // When the REST service is initialized, pre-select the MRS authentication if available, otherwise MySQL const defaultAuthApp = authApps.find((app) => { return app.name === "MRS"; }) ? "MRS" : "MySQL"; const linked = (request.parameters?.init === true) ? app.name === defaultAuthApp : linkedAuthApps.find((linkedApp) => { return linkedApp.id === app.id; }) !== undefined; const result: ICheckboxProperties = { id: app.id, caption: app.name, checkState: linked ? CheckState.Checked : CheckState.Unchecked, }; return { data: result }; }), horizontalSpan: 3, description: "Select one or more REST authentication app. This allows REST users of those applications " + "to authenticate.", }; settingsSection.values.comments = { type: "text", caption: "Comments", value: request.values?.comments as string, multiLine: true, multiLineCount: 4, horizontalSpan: 5, description: "Comments to describe this REST Service.", }; const optionsSection: IDialogSection = { caption: "Options", groupName: "group1", values: { options: { type: "text", caption: "Options:", value: request.values?.options as string, horizontalSpan: 8, multiLine: true, multiLineCount: 8, description: "Additional options in JSON format", }, metadata: { type: "text", caption: "Metadata:", value: request.values?.metadata as string, horizontalSpan: 8, multiLine: true, multiLineCount: 8, description: "Metadata settings in JSON format", }, }, }; const authSection: IDialogSection = { caption: "Authentication Details", groupName: "group1", values: { authPath: { type: "text", caption: "Authentication Path:", value: request.values?.authPath as string, horizontalSpan: 4, description: "The path used for authentication.", }, authCompletedUrl: { type: "text", caption: "Redirection URL:", value: request.values?.authCompletedUrl as string, horizontalSpan: 4, description: "The authentication workflow will redirect to this URL after login.", }, authCompletedUrlValidation: { type: "text", caption: "Redirection URL Validation:", value: request.values?.authCompletedUrlValidation as string, horizontalSpan: 4, description: "A regular expression to validate the /login?onCompletionRedirect " + "parameter set by the app.", }, authCompletedPageContent: { type: "text", caption: "Authentication Completed Page Content:", value: request.values?.authCompletedPageContent as string, multiLine: true, horizontalSpan: 4, description: "If this field is set its content will replace the page content of the " + "/completed page.", }, }, }; let protocol = "HTTPS"; if (request.values?.protocols !== undefined) { const protocols = request.values?.protocols as string[]; if (protocols.length >= 1) { protocol = protocols[protocols.length - 1]; } } const advancedSection: IDialogSection = { caption: "Advanced", groupName: "group1", values: { protocols: { type: "choice", caption: "Supported Protocols", horizontalSpan: 3, choices: ["HTTP", "HTTPS"], value: protocol, description: "The protocol the REST service is accessed on. HTTPS is preferred.", }, }, }; return { id: "mainSection", sections: new Map<string, IDialogSection>([ ["mainSection", mainSection], ["settingsSection", settingsSection], ["optionsSection", optionsSection], ["authSection", authSection], ["advancedSection", advancedSection], ]), }; } private processResults = (dialogValues: IDialogValues): IDictionary => { const mainSection = dialogValues.sections.get("mainSection"); const settingsSection = dialogValues.sections.get("settingsSection"); const optionsSection = dialogValues.sections.get("optionsSection"); const authSection = dialogValues.sections.get("authSection"); const advancedSection = dialogValues.sections.get("advancedSection"); const checkList = (settingsSection?.values.linkedAuthApps as ICheckListDialogValue).checkList; const authAppList = checkList as Array<{ data: ICheckboxProperties; }>; const linkedAuthAppIds = authAppList.filter((app) => { return app.data.checkState === CheckState.Checked; }).map((app) => { return app.data.id!; }); if (mainSection && settingsSection && optionsSection && authSection && advancedSection) { const values: IMrsServiceDialogData = { servicePath: mainSection.values.servicePath.value as string, name: mainSection.values.name.value as string, comments: settingsSection.values.comments.value as string, isCurrent: mainSection.values.makeDefault.value as boolean, enabled: mainSection.values.enabled.value as boolean, published: mainSection.values.published.value as boolean, options: optionsSection.values.options.value as string, authPath: authSection.values.authPath.value as string, authCompletedUrlValidation: authSection.values.authCompletedUrlValidation.value as string, authCompletedUrl: authSection.values.authCompletedUrl.value as string, authCompletedPageContent: authSection.values.authCompletedPageContent.value as string, metadata: optionsSection.values.metadata.value as string, protocols: [advancedSection.values.protocols.value as string], linkedAuthAppIds, }; return values; } return {}; }; }