From 57d629f751f07d77f2cd0afd7ead644c970c4a9a Mon Sep 17 00:00:00 2001 From: Chinthaka Jayatilake <37581983+ChinthakaJ98@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:24:32 +0530 Subject: [PATCH] Add a manage query view to dataservices --- .../service-designer/src/definitions.ts | 11 + .../mi/mi-core/src/state-machine-types.ts | 3 +- .../mi-diagram/src/components/Form/common.ts | 8 +- .../nodes/ReferenceNode/ReferenceNodeModel.ts | 2 +- .../sidePanel/dataServices/query.tsx | 22 +- .../src/project-explorer/activate.ts | 2 +- .../components/ArtifactTest/DataService.ts | 20 +- workspaces/mi/mi-visualizer/src/MainPanel.tsx | 10 +- .../mi/mi-visualizer/src/constants/index.ts | 4 + .../src/utils/DSSResourceForm.ts | 140 ++++++++++- .../mustache-templates/core/DSS.ts | 22 ++ .../mustache-templates/templateUtils.ts | 15 +- .../DataServiceForm/MainPanelForms/index.tsx | 2 +- .../DataServiceForm/QueryServiceDesigner.tsx | 227 ++++++++++++++++++ ...signer.tsx => ResourceServiceDesigner.tsx} | 19 +- .../SidePanelForms/OperationForm.tsx | 50 +++- .../SidePanelForms/QueryForm.tsx | 183 ++++++++++++++ .../SidePanelForms/ResourceForm.tsx | 44 +++- .../views/Overview/ProjectStructureView.tsx | 2 +- .../syntax-tree/src/syntax-tree-interfaces.ts | 8 + 20 files changed, 738 insertions(+), 56 deletions(-) create mode 100644 workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/QueryServiceDesigner.tsx rename workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/{ServiceDesigner.tsx => ResourceServiceDesigner.tsx} (94%) create mode 100644 workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/SidePanelForms/QueryForm.tsx diff --git a/workspaces/common-libs/service-designer/src/definitions.ts b/workspaces/common-libs/service-designer/src/definitions.ts index f64001d2c24..3a699715289 100644 --- a/workspaces/common-libs/service-designer/src/definitions.ts +++ b/workspaces/common-libs/service-designer/src/definitions.ts @@ -73,6 +73,17 @@ export interface Resource { additionalActions?: Item[]; // Additional actions for the resource } +export interface Query { + name: string; + methods: any[]; + isOpen?: boolean; + expandable?: boolean; + additionalInfo?: JSX.Element; // Additional information to be displayed in the resource expanded view + additionalActions?: Item[]; // Additional actions for the resource + params?: ParameterConfig[]; + advancedParams?: Map; +} + export interface PathConfig { path: string; resources: ParameterConfig[]; diff --git a/workspaces/mi/mi-core/src/state-machine-types.ts b/workspaces/mi/mi-core/src/state-machine-types.ts index 267845b8922..f8fa9b1a11b 100644 --- a/workspaces/mi/mi-core/src/state-machine-types.ts +++ b/workspaces/mi/mi-core/src/state-machine-types.ts @@ -65,7 +65,8 @@ export enum MACHINE_VIEW { DefaultEndpointForm = "Default Endpoint Form", DataServiceForm = "Data Service Form", DssDataSourceForm = "DSS Data Source Form", - DSSServiceDesigner = "Data Service Designer", + DSSResourceServiceDesigner = "DSS Resource Designer", + DSSQueryServiceDesigner = "DSS Query Designer", ProjectCreationForm = "Project Creation Form", ImportProjectForm = "Import Project Form", LocalEntryForm = "Local Entry Form", diff --git a/workspaces/mi/mi-diagram/src/components/Form/common.ts b/workspaces/mi/mi-diagram/src/components/Form/common.ts index cfa787ac6cf..871514e565e 100644 --- a/workspaces/mi/mi-diagram/src/components/Form/common.ts +++ b/workspaces/mi/mi-diagram/src/components/Form/common.ts @@ -139,11 +139,13 @@ export const getParamManagerFromValues = (values: any[], keyIndex?: number, valu if (typeof value === 'object' && value !== null) { const paramValues = getParamValues(value); + const key = keyIndex != undefined && keyIndex >= 0 ? typeof value[keyIndex] === 'object' ? value[keyIndex].value : value[keyIndex] : index + 1; return { id: index, - key: keyIndex != undefined && keyIndex >= 0 ? typeof value[keyIndex] === 'object' ? value[keyIndex].value : value[keyIndex] : index + 1, - value: typeof value[valueIndex] === 'object' ? value[valueIndex].value : value[valueIndex], - icon: 'query', paramValues + key: key, + value: (key === "Query" && valueIndex == 4) ? (typeof value[1] === 'object' ? value[1].value : value[1]) : + (typeof value[valueIndex] === 'object' ? value[valueIndex].value : value[valueIndex]), + paramValues }; } else { return { value }; diff --git a/workspaces/mi/mi-diagram/src/components/nodes/ReferenceNode/ReferenceNodeModel.ts b/workspaces/mi/mi-diagram/src/components/nodes/ReferenceNode/ReferenceNodeModel.ts index 861678bbcdc..ff1589a5a8e 100644 --- a/workspaces/mi/mi-diagram/src/components/nodes/ReferenceNode/ReferenceNodeModel.ts +++ b/workspaces/mi/mi-diagram/src/components/nodes/ReferenceNode/ReferenceNodeModel.ts @@ -56,7 +56,7 @@ export class ReferenceNodeModel extends BaseNodeModel { rpcClient.getMiVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { - view: MACHINE_VIEW.DSSServiceDesigner, + view: MACHINE_VIEW.DSSResourceServiceDesigner, documentUri: uri } }); diff --git a/workspaces/mi/mi-diagram/src/components/sidePanel/dataServices/query.tsx b/workspaces/mi/mi-diagram/src/components/sidePanel/dataServices/query.tsx index 3ab6076e21e..529cb12fb59 100644 --- a/workspaces/mi/mi-diagram/src/components/sidePanel/dataServices/query.tsx +++ b/workspaces/mi/mi-diagram/src/components/sidePanel/dataServices/query.tsx @@ -68,8 +68,8 @@ const QueryForm = (props: AddMediatorProps) => { returnGeneratedKeys: sidePanelContext?.formValues?.returnGeneratedKeys || false, keyColumns: sidePanelContext?.formValues?.keyColumns || "", returnUpdatedRowCount: sidePanelContext?.formValues?.returnUpdatedRowCount || false, - forceStoredProc: sidePanelContext?.formValues?.forceStoredProc || false, - forceJdbcBatchRequests: sidePanelContext?.formValues?.forceJdbcBatchRequests || false, + forceStoredProc: sidePanelContext?.formValues?.forceStoredProc === "true" || false, + forceJDBCBatchRequests: sidePanelContext?.formValues?.forceJDBCBatchRequests === "true" || false, }); setIsLoading(false); }, []); @@ -89,7 +89,7 @@ const QueryForm = (props: AddMediatorProps) => { queryProperties.maxFieldSize = values.maxFieldSize; queryProperties.maxRows = values.maxRows; queryProperties.forceStoredProc = values.forceStoredProc; - queryProperties.forceJDBCBatchRequests = values.forceJdbcBatchRequests; + queryProperties.forceJDBCBatchRequests = values.forceJDBCBatchRequests; queryProperties = Object.entries(queryProperties) .filter(([_, value]) => value !== "").map(([key, value]) => ({ key, value })); const updatedQuery = sidePanelContext?.formValues?.queryObject; @@ -184,18 +184,6 @@ const QueryForm = (props: AddMediatorProps) => { <>
- - - ( - - )} - /> - {errors.queryId && {errors.queryId.message.toString()}} - - { ( { field.onChange(e.target.checked) }}>Force JDBC Batch Requests )} /> - {errors.forceJdbcBatchRequests && {errors.forceJdbcBatchRequests.message.toString()}} + {errors.forceJDBCBatchRequests && {errors.forceJDBCBatchRequests.message.toString()}} diff --git a/workspaces/mi/mi-extension/src/project-explorer/activate.ts b/workspaces/mi/mi-extension/src/project-explorer/activate.ts index b121e0b1818..d0f21967de2 100644 --- a/workspaces/mi/mi-extension/src/project-explorer/activate.ts +++ b/workspaces/mi/mi-extension/src/project-explorer/activate.ts @@ -382,7 +382,7 @@ export async function activateProjectExplorer(treeviewId: string, context: Exten commands.registerCommand(COMMANDS.OPEN_DSS_SERVICE_DESIGNER, async (entry: ProjectExplorerEntry | Uri) => { revealWebviewPanel(false); const documentUri = entry instanceof ProjectExplorerEntry ? entry.info?.path : entry.fsPath; - openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.DSSServiceDesigner, documentUri }); + openView(EVENT_TYPE.OPEN_VIEW, { view: MACHINE_VIEW.DSSResourceServiceDesigner, documentUri }); }); commands.registerCommand(COMMANDS.EDIT_K8_CONFIGURATION_COMMAND, async () => { diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/DataService.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/DataService.ts index 3ed2121a8a0..42a0d54188a 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/DataService.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/DataService.ts @@ -67,9 +67,9 @@ export class DataService { await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'Data Services', prevName], true); - const webView = await switchToIFrame('Data Service Designer', this._page); + const webView = await switchToIFrame('DSS Resource Designer', this._page); if (!webView) { - throw new Error("Failed to switch to Data Service Designer iframe"); + throw new Error("Failed to switch to DSS Resource Designer iframe"); } const frame = webView.locator('div#root'); await frame.getByTestId('edit-button').getByLabel('Icon Button').click(); @@ -156,9 +156,9 @@ export class DataService { await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'Data Services', prevName], true); - const webView = await switchToIFrame('Data Service Designer', this._page); + const webView = await switchToIFrame('DSS Resource Designer', this._page); if (!webView) { - throw new Error("Failed to switch to Data Service Designer iframe"); + throw new Error("Failed to switch to DSS Resource Designer iframe"); } const frame = webView.locator('div#root'); await frame.waitFor(); @@ -292,9 +292,9 @@ export class DataService { await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'Data Services', prevName], true); - const webView = await switchToIFrame('Data Service Designer', this._page); + const webView = await switchToIFrame('DSS Resource Designer', this._page); if (!webView) { - throw new Error("Failed to switch to Data Service Designer iframe"); + throw new Error("Failed to switch to DSS Resource Designer iframe"); } const frame = webView.locator('div#root'); await frame.waitFor(); @@ -413,9 +413,9 @@ export class DataService { await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'Data Services', prevName], true); - const webView = await switchToIFrame('Data Service Designer', this._page); + const webView = await switchToIFrame('DSS Resource Designer', this._page); if (!webView) { - throw new Error("Failed to switch to Data Service Designer iframe"); + throw new Error("Failed to switch to DSS Resource Designer iframe"); } const frame = webView.locator('div#root'); await frame.waitFor(); @@ -514,9 +514,9 @@ export class DataService { await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'Data Services', prevName], true); - const webView = await switchToIFrame('Data Service Designer', this._page); + const webView = await switchToIFrame('DSS Resource Designer', this._page); if (!webView) { - throw new Error("Failed to switch to Data Service Designer iframe"); + throw new Error("Failed to switch to DSS Resource Designer iframe"); } const frame = webView.locator('div#root'); await frame.waitFor(); diff --git a/workspaces/mi/mi-visualizer/src/MainPanel.tsx b/workspaces/mi/mi-visualizer/src/MainPanel.tsx index 445a833fd34..0fc83e8fa5a 100644 --- a/workspaces/mi/mi-visualizer/src/MainPanel.tsx +++ b/workspaces/mi/mi-visualizer/src/MainPanel.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState, Suspense } from 'react'; import { POPUP_EVENT_TYPE, PopupMachineStateValue, MACHINE_VIEW, Platform, VisualizerLocation } from '@wso2/mi-core'; import { useVisualizerContext } from '@wso2/mi-rpc-client'; import { ServiceDesignerView } from './views/ServiceDesigner'; -import { DSSServiceDesignerView } from './views/Forms/DataServiceForm/ServiceDesigner'; +import { DSSResourceServiceDesignerView } from './views/Forms/DataServiceForm/ResourceServiceDesigner'; +import { DSSQueryServiceDesignerView } from './views/Forms/DataServiceForm/QueryServiceDesigner'; import { APIWizard, APIWizardProps } from './views/Forms/APIform'; import { EndpointWizard } from './views/Forms/EndpointForm'; import { SequenceWizard } from './views/Forms/SequenceForm'; @@ -376,8 +377,11 @@ const MainPanel = (props: MainPanelProps) => { case MACHINE_VIEW.MockService: setViewComponent(); break; - case MACHINE_VIEW.DSSServiceDesigner: - setViewComponent(); + case MACHINE_VIEW.DSSResourceServiceDesigner: + setViewComponent(); + break; + case MACHINE_VIEW.DSSQueryServiceDesigner: + setViewComponent(); break; case MACHINE_VIEW.Welcome: setViewComponent(); diff --git a/workspaces/mi/mi-visualizer/src/constants/index.ts b/workspaces/mi/mi-visualizer/src/constants/index.ts index 1a7734909bf..b125d643885 100644 --- a/workspaces/mi/mi-visualizer/src/constants/index.ts +++ b/workspaces/mi/mi-visualizer/src/constants/index.ts @@ -69,6 +69,10 @@ export const DSS_TEMPLATES = { EDIT_OPERATION: "edit-dss-operation", EDIT_DESCRIPTION: "edit-dss-description", ADD_QUERY: "add-dss-query", + ADD_FULL_QUERY: "add-full-dss-query", + UPDATE_QUERY_CONFIG: "update-query-config", + UPDATE_QUERY: "update-query", + EDIT_QUERY_REFERENCE: "edit-query-reference" } as const; export enum EndpointTypes { diff --git a/workspaces/mi/mi-visualizer/src/utils/DSSResourceForm.ts b/workspaces/mi/mi-visualizer/src/utils/DSSResourceForm.ts index 086d6d1ca66..ee6016430e4 100644 --- a/workspaces/mi/mi-visualizer/src/utils/DSSResourceForm.ts +++ b/workspaces/mi/mi-visualizer/src/utils/DSSResourceForm.ts @@ -15,12 +15,13 @@ * specific language governing permissions and limitations * under the License. */ -import { Range, DSSResource, DSSOperation } from "@wso2/mi-syntax-tree/lib/src"; +import { Range, DSSResource, DSSOperation, DSSQuery } from "@wso2/mi-syntax-tree/lib/src"; import { RpcClient } from "@wso2/mi-rpc-client"; import { DSS_TEMPLATES } from "../constants"; import { getXML } from "./template-engine/mustache-templates/templateUtils"; import { ResourceFormData, ResourceType } from "../views/Forms/DataServiceForm/SidePanelForms/ResourceForm"; import { OperationFormData, OperationType } from "../views/Forms/DataServiceForm/SidePanelForms/OperationForm"; +import { QueryFormData, QueryType } from "../views/Forms/DataServiceForm/SidePanelForms/QueryForm"; export const generateResourceData = (model: DSSResource): ResourceType => { @@ -29,7 +30,8 @@ export const generateResourceData = (model: DSSResource): ResourceType => { resourceMethod: model.method, description: model.description, enableStreaming: model.enableStreaming, - returnRequestStatus: model.returnRequestStatus + returnRequestStatus: model.returnRequestStatus, + queryId: model.queryId }; return resourceData; }; @@ -39,14 +41,26 @@ export const generateOperationData = (model: DSSOperation): OperationType => { const operationData: OperationType = { operationName: model.name, description: model.description, - enableStreaming: model.enableStreaming + enableStreaming: model.enableStreaming, + queryId: model.queryId }; return operationData; }; +export const generateQueryData = (model: DSSQuery): QueryType => { + + const queryData: QueryType = { + name: model.name, + datasource: model.datasource, + query: model.query, + }; + return queryData; +}; + export const onResourceCreate = (data: ResourceFormData, range: Range, documentUri: string, rpcClient: RpcClient, dbName: string) => { - const queryName = data.resourceMethod.toLowerCase() + "_" + data.resourcePath.replace(/[^a-zA-Z]/g, '').toLowerCase() + "_query"; + const queryName = data.queryId !== "" ? data.queryId : data.resourceMethod.toLowerCase() + "_" + data.resourcePath.replace(/[^a-zA-Z]/g, '').toLowerCase() + "_query"; + const shouldCreateQuery = data.queryId === ""; const formValues = { path: data.resourcePath, @@ -57,7 +71,7 @@ export const onResourceCreate = (data: ResourceFormData, range: Range, documentU query: queryName }; - const queryContent = getXML(DSS_TEMPLATES.ADD_QUERY, { name: queryName, dbName: dbName }); + const queryContent = shouldCreateQuery ? getXML(DSS_TEMPLATES.ADD_QUERY, { name: queryName, dbName: dbName }) : ""; const resourceContent = getXML(DSS_TEMPLATES.ADD_RESOURCE, formValues); rpcClient.getMiDiagramRpcClient().applyEdit({ text: queryContent + resourceContent, @@ -77,7 +91,8 @@ export const onResourceCreate = (data: ResourceFormData, range: Range, documentU export const onOperationCreate = (data: OperationFormData, range: Range, documentUri: string, rpcClient: RpcClient, dbName: string) => { - const queryName = data.operationName.replace(/[^a-zA-Z]/g, '').toLowerCase() + "_query"; + const queryName = data.queryId !== "" ? data.queryId : data.operationName.replace(/[^a-zA-Z]/g, '').toLowerCase() + "_query"; + const shouldCreateQuery = data.queryId === ""; const formValues = { name: data.operationName, @@ -86,7 +101,7 @@ export const onOperationCreate = (data: OperationFormData, range: Range, documen query: queryName }; - const queryContent = getXML(DSS_TEMPLATES.ADD_QUERY, { name: queryName, dbName: dbName }); + const queryContent = shouldCreateQuery ? getXML(DSS_TEMPLATES.ADD_QUERY, { name: queryName, dbName: dbName }) : ""; const operationContent = getXML(DSS_TEMPLATES.ADD_OPERATION, formValues); rpcClient.getMiDiagramRpcClient().applyEdit({ text: queryContent + operationContent, @@ -104,10 +119,45 @@ export const onOperationCreate = (data: OperationFormData, range: Range, documen }); }; +export const onQueryCreate = (data: QueryFormData, range: Range, documentUri: string, rpcClient: RpcClient, dbName: string) => { + + const formValues = { + name: data.name, + datasource: data.datasource, + query: data.query + }; + + getQueryType(rpcClient, documentUri, data.datasource) + .then((queryType) => { + const queryContent = getXML(DSS_TEMPLATES.ADD_FULL_QUERY, { + ...formValues, + isExpression: queryType === "expression" + }); + + return rpcClient.getMiDiagramRpcClient().applyEdit({ + text: queryContent, + documentUri, + range: { + start: { + line: range.end.line, + character: range.end.character, + }, + end: { + line: range.end.line, + character: range.end.character, + } + } + }); + }); + +}; + export const onResourceEdit = (data: ResourceFormData, selectedResource: any, documentUri: string, rpcClient: RpcClient) => { const resourceStartTagRange = selectedResource.range.startTagRange; const descriptionStartTagRange = selectedResource.description ? selectedResource.description.range.startTagRange : undefined; const descriptionEndTagRange = selectedResource.description ? selectedResource.description.range.endTagRange : undefined; + const referenceQueryTagRange = selectedResource.callQuery ? selectedResource.callQuery.range.startTagRange : undefined; + const isReferenceSelfClosed = selectedResource.callQuery?.selfClosed ?? true; const formValues = { path: data.resourcePath, method: data.resourceMethod, @@ -118,6 +168,7 @@ export const onResourceEdit = (data: ResourceFormData, selectedResource: any, do const resourceXML = getXML(DSS_TEMPLATES.EDIT_RESOURCE, formValues); const descriptionXML = data.description === "" ? "" : getXML(DSS_TEMPLATES.EDIT_DESCRIPTION, {description: data.description}); + const queryReferenceXML = getXML(DSS_TEMPLATES.EDIT_QUERY_REFERENCE, { queryId: data.queryId, isSelfClosed: isReferenceSelfClosed }); rpcClient.getMiDiagramRpcClient().applyEdit({ text: resourceXML, @@ -132,13 +183,23 @@ export const onResourceEdit = (data: ResourceFormData, selectedResource: any, do end: descriptionEndTagRange ? descriptionEndTagRange.end : resourceStartTagRange.end } }) - }) + }).then(async () => { + if (referenceQueryTagRange) { + await rpcClient.getMiDiagramRpcClient().applyEdit({ + text: queryReferenceXML, + documentUri: documentUri, + range: referenceQueryTagRange + }) + } + }); }; export const onOperationEdit = (data: OperationFormData, selectedOperation: any, documentUri: string, rpcClient: RpcClient) => { const operationStartTagRange = selectedOperation.range.startTagRange; const descriptionStartTagRange = selectedOperation.description ? selectedOperation.description.range.startTagRange : undefined; const descriptionEndTagRange = selectedOperation.description ? selectedOperation.description.range.endTagRange : undefined; + const referenceQueryTagRange = selectedOperation.callQuery ? selectedOperation.callQuery.range.startTagRange : undefined; + const isReferenceSelfClosed = selectedOperation.callQuery?.selfClosed ?? true; const formValues = { name: data.operationName, enableStreaming: data.enableStreaming ? undefined : !data.enableStreaming, @@ -147,6 +208,7 @@ export const onOperationEdit = (data: OperationFormData, selectedOperation: any, const operationXML = getXML(DSS_TEMPLATES.EDIT_OPERATION, formValues); const descriptionXML = data.description === "" ? "" : getXML(DSS_TEMPLATES.EDIT_DESCRIPTION, {description: data.description}); + const queryReferenceXML = getXML(DSS_TEMPLATES.EDIT_QUERY_REFERENCE, { queryId: data.queryId, isSelfClosed: isReferenceSelfClosed }); rpcClient.getMiDiagramRpcClient().applyEdit({ text: operationXML, @@ -160,6 +222,66 @@ export const onOperationEdit = (data: OperationFormData, selectedOperation: any, start: descriptionStartTagRange ? descriptionStartTagRange.start : operationStartTagRange.end, end: descriptionEndTagRange ? descriptionEndTagRange.end : operationStartTagRange.end } - }) + }).then(async () => { + if (referenceQueryTagRange) { + await rpcClient.getMiDiagramRpcClient().applyEdit({ + text: queryReferenceXML, + documentUri: documentUri, + range: referenceQueryTagRange + }) + } + }); }) }; + +export const onQueryEdit = (data: QueryFormData, selectedQuery: any, documentUri: string, rpcClient: RpcClient) => { + const sqlOrExpressionStartTagRange = selectedQuery?.sql ? selectedQuery.sql?.range?.startTagRange : + selectedQuery?.expression ? selectedQuery.expression?.range?.startTagRange : undefined; + const sqlOrExpressionEndTagRange = selectedQuery?.sql ? selectedQuery.sql?.range?.endTagRange : + selectedQuery?.expression ? selectedQuery.expression?.range?.endTagRange : undefined; + const formValues = { + name: data.name, + datasource: data.datasource, + query: data.query + }; + + getQueryType(rpcClient, documentUri, data.datasource) + .then((queryType) => { + return rpcClient.getMiDiagramRpcClient().applyEdit({ + text: getXML(DSS_TEMPLATES.UPDATE_QUERY_CONFIG, formValues), + documentUri, + range: selectedQuery.range.startTagRange, + }).then(() => ({ queryType })); + }) + .then(({ queryType }) => { + return rpcClient.getMiDiagramRpcClient().applyEdit({ + text: getXML(DSS_TEMPLATES.UPDATE_QUERY, { + query: data.query, + isExpression: queryType === "expression" + }), + documentUri, + range: { + start: sqlOrExpressionStartTagRange ? sqlOrExpressionStartTagRange.start : selectedQuery.range.startTagRange.end, + end: sqlOrExpressionEndTagRange ? sqlOrExpressionEndTagRange.end : selectedQuery.range.startTagRange.end + } + }); + }); + +}; + +async function getQueryType(rpcClient: RpcClient, documentUri: string, datasource: string): Promise { + let queryType = "sql"; + const existingDataService = await rpcClient.getMiDiagramRpcClient().getDataService({ path: documentUri }); + existingDataService.datasources.forEach((ds) => { + if (ds.dataSourceName === datasource) { + const propertyKeys: string[] = []; + ds.datasourceProperties.forEach((attr: any) => { + propertyKeys.push(attr.key); + }); + if (propertyKeys.includes("mongoDB_servers")) { + queryType = "expression"; + } + } + }); + return queryType; +} diff --git a/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/core/DSS.ts b/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/core/DSS.ts index 78a052f0027..41ae3b61f5f 100644 --- a/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/core/DSS.ts +++ b/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/core/DSS.ts @@ -40,6 +40,24 @@ export function getAddQuery() { ` } +export function getAddFullQuery() { + return ` +{{#isExpression}}{{query}}{{/isExpression}}{{^isExpression}}{{query}}{{/isExpression}} +` +} + +export function getQueryConfig() { + return `` +} + +export function getSQLQuery() { + return `{{query}}` +} + +export function getExpressionQuery() { + return `{{query}}` +} + export function getEditOperationTemplate() { return `` } @@ -47,3 +65,7 @@ export function getEditOperationTemplate() { export function getEditDescriptionTemplate() { return `{{description}}` } + +export function getEditQueryReferenceTemplate() { + return `` +} diff --git a/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/templateUtils.ts b/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/templateUtils.ts index 15d6de6821b..f0bbabeaade 100644 --- a/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/templateUtils.ts +++ b/workspaces/mi/mi-visualizer/src/utils/template-engine/mustache-templates/templateUtils.ts @@ -33,7 +33,12 @@ import { getEditOperationTemplate, getEditResourceTemplate, getEditDescriptionTemplate, - getAddQuery + getEditQueryReferenceTemplate, + getAddQuery, + getAddFullQuery, + getQueryConfig, + getExpressionQuery, + getSQLQuery } from "./core/DSS"; export function getXML(name: string, data: { [key: string]: any }) { @@ -62,8 +67,16 @@ export function getXML(name: string, data: { [key: string]: any }) { return Mustache.render(getEditOperationTemplate(), data); case DSS_TEMPLATES.EDIT_DESCRIPTION: return Mustache.render(getEditDescriptionTemplate(), data); + case DSS_TEMPLATES.EDIT_QUERY_REFERENCE: + return Mustache.render(getEditQueryReferenceTemplate(), data); case DSS_TEMPLATES.ADD_QUERY: return Mustache.render(getAddQuery(), data); + case DSS_TEMPLATES.ADD_FULL_QUERY: + return Mustache.render(getAddFullQuery(), data); + case DSS_TEMPLATES.UPDATE_QUERY_CONFIG: + return Mustache.render(getQueryConfig(), data); + case DSS_TEMPLATES.UPDATE_QUERY: + return data.isExpression ? Mustache.render(getExpressionQuery(), data) : Mustache.render(getSQLQuery(), data); default: return ""; } diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/MainPanelForms/index.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/MainPanelForms/index.tsx index df1110cdf23..3e0b336a8a8 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/MainPanelForms/index.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/MainPanelForms/index.tsx @@ -273,7 +273,7 @@ export function DataServiceWizard(props: DataServiceWizardProps) { } else if (isNewDataService) { rpcClient.getMiVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { view: MACHINE_VIEW.Overview } }); } else { - rpcClient.getMiVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { view: MACHINE_VIEW.DSSServiceDesigner, documentUri: props.path } }); + rpcClient.getMiVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { view: MACHINE_VIEW.DSSResourceServiceDesigner, documentUri: props.path } }); } }; diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/QueryServiceDesigner.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/QueryServiceDesigner.tsx new file mode 100644 index 00000000000..ff615c352df --- /dev/null +++ b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/QueryServiceDesigner.tsx @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from "react"; +import { EVENT_TYPE, MACHINE_VIEW } from "@wso2/mi-core"; +import { useVisualizerContext } from "@wso2/mi-rpc-client"; +import { Resource, Service, ServiceDesigner } from "@wso2/service-designer"; +import { Item } from "@wso2/ui-toolkit"; +import { View, ViewHeader, ViewContent } from "../../../components/View"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { Codicon } from "@wso2/ui-toolkit"; +import { generateQueryData, onQueryCreate, onQueryEdit } from "../../../utils/DSSResourceForm"; +import { QueryForm, QueryFormData } from "../../Forms/DataServiceForm/SidePanelForms/QueryForm"; + +interface ServiceDesignerProps { + syntaxTree: any; + documentUri: string; +} +export function DSSQueryServiceDesignerView({ syntaxTree, documentUri }: ServiceDesignerProps) { + const { rpcClient } = useVisualizerContext(); + const [queryServiceModel, setQueryServiceModel] = React.useState(null); + const [isQueryFormOpen, setQueryFormOpen] = React.useState(false); + const [queryBodyRange, setQueryBodyRange] = React.useState(null); + const [formData, setFormData] = React.useState(null); + const [mode, setMode] = React.useState<"create" | "edit">("create"); + const [selectedQuery, setSelectedQuery] = React.useState(null); + + const getQueries = (st: any): Resource[] => { + let queries: any = st.data.queries ?? []; + return queries.map((query: any) => { + const value: any = { + methods: [query.useConfig], + path: query.id, + position: { + startLine: query.range.startTagRange.start.line, + startColumn: query.range.startTagRange.start.character, + endLine: query.range.endTagRange.end.line, + endColumn: query.range.endTagRange.end.character, + }, + expandable: false, + }; + const currentQuery: any = { + name: query.id, + datasource: query.useConfig, + query: query.sql?.value ?? query.expression?.value ?? "", + position: { + startLine: query.range.startTagRange.start.line, + startColumn: query.range.startTagRange.start.character, + endLine: query.range.endTagRange.end.line, + endColumn: query.range.endTagRange.end.character, + }, + expandable: false, + }; + const goToSourceAction: Item = { + id: "go-to-source", + label: "Go to Source", + onClick: () => highlightCode(value, true), + }; + const editAction: Item = { + id: "edit", + label: "Edit", + onClick: () => { + setFormData(generateQueryData(currentQuery)); + setSelectedQuery(query); + setMode("edit"); + setQueryFormOpen(true); + }, + }; + const deleteAction: Item = { + id: "delete", + label: "Delete", + onClick: () => handleDelete(query) + }; + const moreActions: Item[] = [goToSourceAction, editAction, deleteAction]; + return { + ...value, + additionalActions: moreActions, + }; + }); + }; + + useEffect(() => { + const st = syntaxTree; + + const queries: Resource[] = getQueries(st); + setQueryBodyRange({ + start: st.data.range.startTagRange.end, + end: st.data.range.endTagRange.start + }); + const queryModel: Service = { + path: st.context, + resources: queries + } + setQueryServiceModel(queryModel); + + }, [syntaxTree, documentUri]); + + const highlightCode = (query: Resource, force?: boolean) => { + rpcClient.getMiDiagramRpcClient().highlightCode({ + range: { + start: { + line: query.position.startLine, + character: query.position.startColumn, + + }, + end: { + line: query.position.endLine, + character: query.position.endColumn, + }, + }, + force: force, + }); + }; + + const openDiagram = (query: Resource) => { + const href = query.path; + if (!href) { + rpcClient.getMiDiagramRpcClient().showErrorMessage({ message: "Cannot find the query for selected resource" }); + return; + } + rpcClient.getMiVisualizerRpcClient().openView({ type: EVENT_TYPE.OPEN_VIEW, location: { view: MACHINE_VIEW.DataServiceView, documentUri: documentUri, identifier: href } }) + } + + const handleManageResources = () => { + rpcClient.getMiVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.DSSResourceServiceDesigner, + documentUri: documentUri + } + }); + } + + const handleQueryAdd = () => { + setMode("create"); + setQueryFormOpen(true); + }; + + const handleCancel = () => { + setQueryFormOpen(false); + }; + + const handleQueryCreate = (formData: QueryFormData) => { + switch (formData.mode) { + case "create": + let dbName = ""; + if (syntaxTree.data.configs !== undefined && syntaxTree.data.configs !== null && syntaxTree.data.configs.length > 0) { + dbName = syntaxTree.data.configs[0].id; + } + onQueryCreate(formData, queryBodyRange, documentUri, rpcClient, dbName); + break; + case "edit": + onQueryEdit(formData, selectedQuery, documentUri, rpcClient); + break; + } + setQueryFormOpen(false); + }; + + const handleDelete = (currentQuery: any) => { + rpcClient.getMiDiagramRpcClient().applyEdit({ + text: "", + documentUri: documentUri, + range: { + start: { + line: currentQuery.range.startTagRange.start.line, + character: currentQuery.range.startTagRange.start.character, + }, + end: { + line: currentQuery.range.endTagRange.end.line, + character: currentQuery.range.endTagRange.end.character, + }, + }, + }); + }; + + const handleQueryClick = (query: Resource) => { + highlightCode(query); + openDiagram(query); + }; + + return ( + <> + {queryServiceModel && ( + + + + Manage Resources + + + Add Query + + + + + + + )} + + + ); +} diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ServiceDesigner.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ResourceServiceDesigner.tsx similarity index 94% rename from workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ServiceDesigner.tsx rename to workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ResourceServiceDesigner.tsx index edddcc6f553..14c9917257d 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ServiceDesigner.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/ResourceServiceDesigner.tsx @@ -33,7 +33,7 @@ interface ServiceDesignerProps { syntaxTree: any; documentUri: string; } -export function DSSServiceDesignerView({ syntaxTree, documentUri }: ServiceDesignerProps) { +export function DSSResourceServiceDesignerView({ syntaxTree, documentUri }: ServiceDesignerProps) { const { rpcClient } = useVisualizerContext(); const [resourceServiceModel, setResourceServiceModel] = React.useState(null); const [operationServiceModel, setOperationServiceModel] = React.useState(null); @@ -68,6 +68,7 @@ export function DSSServiceDesignerView({ syntaxTree, documentUri }: ServiceDesig description: resource.description ? resource.description.textNode : "", enableStreaming: !resource.disableStreaming, returnRequestStatus: resource.returnRequestStatus ?? false, + queryId: resource.callQuery ? resource.callQuery.href : "", position: { startLine: resource.range.startTagRange.start.line, startColumn: resource.range.startTagRange.start.character, @@ -120,6 +121,7 @@ export function DSSServiceDesignerView({ syntaxTree, documentUri }: ServiceDesig }; const currentOperation: any = { name: operation.name, + queryId: operation.callQuery ? operation.callQuery.href : "", description: operation.description ? operation.description.textNode : "", enableStreaming: !operation.disableStreaming, position: { @@ -232,6 +234,16 @@ export function DSSServiceDesignerView({ syntaxTree, documentUri }: ServiceDesig setOperationFormOpen(true); }; + const handleManageQueries = () => { + rpcClient.getMiVisualizerRpcClient().openView({ + type: EVENT_TYPE.OPEN_VIEW, + location: { + view: MACHINE_VIEW.DSSQueryServiceDesigner, + documentUri: documentUri + } + }); + } + const handleCancel = () => { setResourceFormOpen(false); setOperationFormOpen(false); @@ -306,7 +318,10 @@ export function DSSServiceDesignerView({ syntaxTree, documentUri }: ServiceDesig <> {(resourceServiceModel || operationServiceModel) && ( - + + + Manage Queries + {showResources ? ( diff --git a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/SidePanelForms/OperationForm.tsx b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/SidePanelForms/OperationForm.tsx index 188d1671805..9fb8a167e1e 100644 --- a/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/SidePanelForms/OperationForm.tsx +++ b/workspaces/mi/mi-visualizer/src/views/Forms/DataServiceForm/SidePanelForms/OperationForm.tsx @@ -21,8 +21,11 @@ import { Button, TextField, SidePanel, SidePanelTitleContainer, SidePanelBody, C import * as yup from "yup"; import styled from "@emotion/styled"; import { SIDE_PANEL_WIDTH } from "../../../../constants"; -import { useForm } from "react-hook-form"; +import { useForm, Controller } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; +import { Keylookup } from "@wso2/mi-diagram"; +import { openPopup } from "@wso2/mi-diagram/lib/components/Form/common"; +import { useVisualizerContext } from '@wso2/mi-rpc-client'; const ActionContainer = styled.div` display: flex; @@ -60,20 +63,31 @@ namespace Section { type OperationFields = { operationName: string; + queryId?: string; description: string; enableStreaming: boolean; + useExistingQuery?: boolean; }; const newOperation: OperationFields = { operationName: "", + queryId: "", description: "", - enableStreaming: false + enableStreaming: false, + useExistingQuery: false }; const schema = yup.object({ operationName: yup.string().required("Operation name is required"), + queryId: yup.string().when("useExistingQuery", { + is: true, + then: (schema) => + schema.required("Query ID is required"), + otherwise: (schema) => schema.notRequired(), + }), description: yup.string().notRequired(), - enableStreaming: yup.boolean().notRequired() + enableStreaming: yup.boolean().notRequired(), + useExistingQuery: yup.boolean().notRequired() }); export type OperationType = yup.InferType; @@ -90,10 +104,11 @@ type OperationFormProps = { onSave: (data: OperationFormData) => void; }; -export const OperationForm = ({ isOpen, onCancel, onSave, formData }: OperationFormProps) => { +export const OperationForm = ({ isOpen, onCancel, onSave, formData, documentUri }: OperationFormProps) => { const { control, handleSubmit, + watch, formState: { errors, isDirty }, register, reset @@ -103,6 +118,8 @@ export const OperationForm = ({ isOpen, onCancel, onSave, formData }: OperationF mode: "onChange", }); + const { rpcClient } = useVisualizerContext(); + useEffect(() => { if (isOpen && formData) { reset(formData); @@ -152,6 +169,31 @@ export const OperationForm = ({ isOpen, onCancel, onSave, formData }: OperationF size={150} {...renderProps('operationName')} /> + { !formData && ( + + )} + + { (formData || watch("useExistingQuery")) && ( + ( + { + openPopup(rpcClient, "dssQuery", fetchItems, handleValueChange, documentUri, { datasource: undefined }); + }} + onValueChange={field.onChange} + required={true} + /> + )} + /> + )}