diff --git a/src/api/api.ts b/src/api/api.ts index b78254f4..d2f25a3f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -63,6 +63,7 @@ import type { LiveExchange, ContextSystem, IntegrationSystemType, + UsedProperty, DiagnosticValidation, BulkDeploymentRequest, BulkDeploymentResult, @@ -501,6 +502,8 @@ export interface Api { ): Promise>; createSecret(secretName: string): Promise>; downloadHelmChart(secretName: string): Promise; + + getUsedProperties(chainId: string): Promise; } export const api: Api = isVsCode ? new VSCodeExtensionApi() : new RestApi(); diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index 55ec1019..31f13791 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -1271,3 +1271,36 @@ export enum BulkDeploymentStatus { CREATED = "CREATED", IGNORED = "IGNORED", } + +export enum UsedPropertySource { + HEADER = 'HEADER', + EXCHANGE_PROPERTY = 'EXCHANGE_PROPERTY' +} + +export enum UsedPropertyType { + STRING = 'STRING', + NUMBER = 'NUMBER', + BOOLEAN = 'BOOLEAN', + OBJECT = 'OBJECT', + UNKNOWN_TYPE = 'UNKNOWN_TYPE', +} + +export enum UsedPropertyElementOperation { + GET = 'GET', + SET = 'SET', +} + +export type UsedProperty = { + name: string; + source: UsedPropertySource; + type: UsedPropertyType; + isArray: boolean; + relatedElements: { [id: string]: UsedPropertyElement }; +}; + +export interface UsedPropertyElement { + id: string; + name: string; + type: string; + operations: UsedPropertyElementOperation[]; +} diff --git a/src/api/rest/restApi.ts b/src/api/rest/restApi.ts index 2ea95e2b..233af178 100644 --- a/src/api/rest/restApi.ts +++ b/src/api/rest/restApi.ts @@ -74,6 +74,7 @@ import { BulkDeploymentResult, ImportVariablesResult, VariableImportPreview, + UsedProperty } from "../apiTypes.ts"; import { Api } from "../api.ts"; import { getFileFromResponse } from "../../misc/download-utils.ts"; @@ -523,7 +524,7 @@ export class RestApi implements Api { getChain = async (id: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/chains/${id}`, + `/api/v1/${getAppName()}/catalog/chains/${id}`, ); return response.data; }; @@ -903,7 +904,7 @@ export class RestApi implements Api { maskedFieldId: string, ): Promise => { await this.instance.delete( - `${this.v1()}/catalog/chains/${chainId}/masking/field/${maskedFieldId}`, + `/api/v1/${getAppName()}/catalog/chains/${chainId}/masking/field/${maskedFieldId}`, ); }; @@ -913,7 +914,7 @@ export class RestApi implements Api { changes: Partial>, ): Promise => { const response = await this.instance.put( - `${this.v1()}/catalog/chains/${chainId}/masking/field/${maskedFieldId}`, + `/api/v1/${getAppName()}/catalog/chains/${chainId}/masking/field/${maskedFieldId}`, changes, ); return response.data; @@ -924,7 +925,7 @@ export class RestApi implements Api { filters: SessionFilterAndSearchRequest, paginationOptions: PaginationOptions, ): Promise => { - const prefix = `${this.v1()}/sessions-management/sessions`; + const prefix = `/api/v1/${getAppName()}/sessions-management/sessions`; const url = chainId ? `${prefix}/chains/${chainId}` : prefix; const params: Record = {}; if (paginationOptions.offset) { @@ -943,7 +944,7 @@ export class RestApi implements Api { deleteSessions = async (sessionIds: string[]): Promise => { await this.instance.post( - `${this.v1()}/sessions-management/sessions/bulk-delete`, + `/api/v1/${getAppName()}/sessions-management/sessions/bulk-delete`, sessionIds, ); }; @@ -951,14 +952,14 @@ export class RestApi implements Api { deleteSessionsByChainId = async ( chainId: string | undefined, ): Promise => { - const prefix = `${this.v1()}/sessions-management/sessions`; + const prefix = `/api/v1/${getAppName()}/sessions-management/sessions`; const url = chainId ? `${prefix}/chains/${chainId}` : prefix; await this.instance.delete(url); }; exportSessions = async (sessionIds: string[]): Promise => { const response = await this.instance.post( - `${this.v1()}/sessions-management/sessions/export`, + `/api/v1/${getAppName()}/sessions-management/sessions/export`, sessionIds, { responseType: "blob" }, ); @@ -972,7 +973,7 @@ export class RestApi implements Api { headers.set("Content-Type", "multipart/form-data"); headers.set("accept", "*/*"); const response = await this.instance.post( - `${this.v1()}/sessions-management/sessions/import`, + `/api/v1/${getAppName()}/sessions-management/sessions/import`, formData, { headers }, ); @@ -984,14 +985,14 @@ export class RestApi implements Api { sessionId: string, ): Promise => { await this.instance.post( - `${this.v1()}/engine/chains/${chainId}/sessions/${sessionId}/retry`, + `/api/v1/${getAppName()}/engine/chains/${chainId}/sessions/${sessionId}/retry`, null, ); }; getSession = async (sessionId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/sessions-management/sessions/${sessionId}`, + `/api/v1/${getAppName()}/sessions-management/sessions/${sessionId}`, ); return response.data; }; @@ -1000,7 +1001,7 @@ export class RestApi implements Api { sessionIds: string[], ): Promise => { const response = await this.instance.get( - `${this.v1()}/engine/sessions`, + `/api/v1/${getAppName()}/engine/sessions`, { params: { ids: sessionIds }, paramsSerializer: { @@ -1016,14 +1017,14 @@ export class RestApi implements Api { sessionId: string, ): Promise => { return this.instance.post( - `${this.v1()}/engine/chains/${chainId}/sessions/${sessionId}/retry`, + `/api/v1/${getAppName()}/engine/chains/${chainId}/sessions/${sessionId}/retry`, null, ); }; getFolder = async (folderId: string): Promise => { const response = await this.instance.get( - `${this.v2()}/catalog/folders/${folderId}`, + `/api/v2/${getAppName()}/catalog/folders/${folderId}`, ); return response.data; }; @@ -1033,7 +1034,7 @@ export class RestApi implements Api { openedFolderId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/folders/`, + `/api/v1/${getAppName()}/catalog/folders/`, { params: { filter: filter, @@ -1046,21 +1047,21 @@ export class RestApi implements Api { getPathToFolder = async (folderId: string): Promise => { const response = await this.instance.get( - `${this.v2()}/catalog/folders/${folderId}/path`, + `/api/v2/${getAppName()}/catalog/folders/${folderId}/path`, ); return response.data; }; getPathToFolderByName = async (folderName: string): Promise => { const response = await this.instance.get( - `${this.v2()}/catalog/folders/path?name=${folderName}`, + `/api/v2/${getAppName()}/catalog/folders/path?name=${folderName}`, ); return response.data; }; createFolder = async (request: CreateFolderRequest): Promise => { const response = await this.instance.post( - `${this.v2()}/catalog/folders`, + `/api/v2/${getAppName()}/catalog/folders`, request, ); return response.data; @@ -1071,19 +1072,21 @@ export class RestApi implements Api { changes: UpdateFolderRequest, ): Promise => { const response = await this.instance.put( - `${this.v2()}/catalog/folders/${folderId}`, + `/api/v2/${getAppName()}/catalog/folders/${folderId}`, changes, ); return response.data; }; deleteFolder = async (folderId: string): Promise => { - await this.instance.delete(`${this.v2()}/catalog/folders/${folderId}`); + await this.instance.delete( + `/api/v2/${getAppName()}/catalog/folders/${folderId}`, + ); }; deleteFolders = async (folderIds: string[]): Promise => { await this.instance.post( - `${this.v2()}/catalog/folders/bulk-delete`, + `/api/v2/${getAppName()}/catalog/folders/bulk-delete`, folderIds, ); }; @@ -1092,7 +1095,7 @@ export class RestApi implements Api { request: ListFolderRequest, ): Promise<(FolderItem | ChainItem)[]> => { const response = await this.instance.post<(FolderItem | ChainItem)[]>( - `${this.v2()}/catalog/folders/list`, + `/api/v2/${getAppName()}/catalog/folders/list`, request, ); return response.data; @@ -1107,7 +1110,7 @@ export class RestApi implements Api { targetId: targetFolderId, }; const response = await this.instance.post( - `${this.v2()}/catalog/folders/move`, + `/api/v2/${getAppName()}/catalog/folders/move`, request, ); return response.data; @@ -1115,7 +1118,7 @@ export class RestApi implements Api { getNestedChains = async (folderId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/folders/${folderId}/chains`, + `/api/v1/${getAppName()}/catalog/folders/${folderId}/chains`, ); return response.data; }; @@ -1124,7 +1127,7 @@ export class RestApi implements Api { chainIds: string[], ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/chains/used-systems`, + `/api/v1/${getAppName()}/catalog/chains/used-systems`, { params: { chainIds }, paramsSerializer: { @@ -1137,7 +1140,7 @@ export class RestApi implements Api { getChainsUsedByService = async (systemId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/chains/systems/${systemId}`, + `/api/v1/${getAppName()}/catalog/chains/systems/${systemId}`, ); return response.data; }; @@ -1154,7 +1157,7 @@ export class RestApi implements Api { formData.append("usedSystemModelIds", modelIds.join(",")); } const response = await this.instance.post( - `${this.v1()}/systems-catalog/export/system`, + `/api/v1/${getAppName()}/systems-catalog/export/system`, formData, { headers: { @@ -1179,7 +1182,7 @@ export class RestApi implements Api { params["specificationGroupId"] = specificationGroupId.join(","); } const response = await this.instance.get( - `${this.v1()}/systems-catalog/export/specifications`, + `/api/v1/${getAppName()}/systems-catalog/export/specifications`, { params, headers: { @@ -1215,7 +1218,7 @@ export class RestApi implements Api { } const response = await this.instance.get( - `${this.v1()}/catalog/export/api-spec`, + `/api/v1/${getAppName()}/catalog/export/api-spec`, { params: { ...params, externalRoutes, specificationType, format }, headers: { @@ -1251,7 +1254,7 @@ export class RestApi implements Api { withSpec: boolean, ): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/systems`, + `/api/v1/${getAppName()}/systems-catalog/systems`, { params: { modelType, withSpec }, }, @@ -1261,7 +1264,7 @@ export class RestApi implements Api { createService = async (system: SystemRequest): Promise => { const response = await this.instance.post( - `${this.v1()}/systems-catalog/systems`, + `/api/v1/${getAppName()}/systems-catalog/systems`, system, ); return response.data; @@ -1272,7 +1275,7 @@ export class RestApi implements Api { envRequest: EnvironmentRequest, ): Promise => { const response = await this.instance.post( - `${this.v1()}/systems-catalog/systems/${systemId}/environments`, + `/api/v1/${getAppName()}/systems-catalog/systems/${systemId}/environments`, envRequest, ); return response.data; @@ -1284,7 +1287,7 @@ export class RestApi implements Api { envRequest: EnvironmentRequest, ): Promise => { const response = await this.instance.put( - `${this.v1()}/systems-catalog/systems/${systemId}/environments/${environmentId}`, + `/api/v1/${getAppName()}/systems-catalog/systems/${systemId}/environments/${environmentId}`, envRequest, ); return response.data; @@ -1295,13 +1298,13 @@ export class RestApi implements Api { environmentId: string, ): Promise => { await this.instance.delete( - `${this.v1()}/systems-catalog/systems/${systemId}/environments/${environmentId}`, + `/api/v1/${getAppName()}/systems-catalog/systems/${systemId}/environments/${environmentId}`, ); }; deleteService = async (serviceId: string): Promise => { await this.instance.delete( - `${this.v1()}/systems-catalog/systems/${serviceId}`, + `/api/v1/${getAppName()}/systems-catalog/systems/${serviceId}`, ); }; @@ -1309,7 +1312,7 @@ export class RestApi implements Api { const formData: FormData = new FormData(); formData.append("file", file, file.name); const response = await this.instance.post( - `${this.v3()}/import/preview`, + `/api/${getAppName()}/v3/import/preview`, formData, { headers: { @@ -1335,7 +1338,7 @@ export class RestApi implements Api { formData.append("validateByHash", validateByHash.toString()); } const response = await this.instance.post( - `${this.v3()}/import`, + `/api/${getAppName()}/v3/import`, formData, { headers: { @@ -1349,14 +1352,14 @@ export class RestApi implements Api { getImportStatus = async (importId: string): Promise => { const response = await this.instance.get( - `${this.v3()}/import/${importId}`, + `/api/${getAppName()}/v3/import/${importId}`, ); return response.data; }; getEvents = async (lastEventId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/events`, + `/api/v1/${getAppName()}/catalog/events`, { params: { lastEventId: lastEventId, @@ -1371,23 +1374,36 @@ export class RestApi implements Api { engineHost: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/domains/${domain}/engines/${engineHost}/deployments`, + `/api/v1/${getAppName()}/catalog/domains/${domain}/engines/${engineHost}/deployments`, ); return response.data; }; getEnginesByDomain = async (domain: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/domains/${domain}/engines`, + `/api/v1/${getAppName()}/catalog/domains/${domain}/engines`, ); return response.data; }; loadCatalogActionsLog = async ( searchRequest: ActionLogSearchRequest, + ): Promise => { + return await this.loadActionsLog("catalog", searchRequest); + }; + + loadVariablesManagementActionsLog = async ( + searchRequest: ActionLogSearchRequest, + ): Promise => { + return await this.loadActionsLog("variables-management", searchRequest); + }; + + loadActionsLog = async ( + serviceName: string, + searchRequest: ActionLogSearchRequest, ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/actions-log`, + `/api/v1/${getAppName()}/${serviceName}/actions-log`, searchRequest, ); return response.data; @@ -1395,12 +1411,25 @@ export class RestApi implements Api { exportCatalogActionsLog = async ( params: LogExportRequestParams, + ): Promise => { + return await this.exportActionLog("catalog", params); + }; + + exportVariablesManagementActionsLog = async ( + params: LogExportRequestParams, + ): Promise => { + return await this.exportActionLog("variables-management", params); + }; + + exportActionLog = async ( + serviceName: string, + params: LogExportRequestParams, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/actions-log/export`, + `/api/v1/${getAppName()}/${serviceName}/actions-log/export`, { responseType: "blob", - params, + params: params, }, ); return response.data; @@ -1408,7 +1437,7 @@ export class RestApi implements Api { getContextServices = async (): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/context-system`, + `/api/v1/${getAppName()}/catalog/context-system`, ); const result = response.data; response.data.map( @@ -1419,7 +1448,7 @@ export class RestApi implements Api { getContextService = async (id: string): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/context-system/${id}`, + `/api/v1/${getAppName()}/catalog/context-system/${id}`, ); return response.data; }; @@ -1428,7 +1457,7 @@ export class RestApi implements Api { system: Pick, ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/context-system`, + `/api/v1/${getAppName()}/catalog/context-system`, system, ); return response.data; @@ -1439,7 +1468,7 @@ export class RestApi implements Api { data: Partial, ): Promise => { const response = await this.instance.put( - `${this.v1()}/catalog/context-system/${id}`, + `/api/v1/${getAppName()}/catalog/context-system/${id}`, data, ); return response.data; @@ -1447,7 +1476,7 @@ export class RestApi implements Api { deleteContextService = async (serviceId: string): Promise => { await this.instance.delete( - `${this.v1()}/catalog/context-system/${serviceId}`, + `/api/v1/${getAppName()}/catalog/context-system/${serviceId}`, ); }; @@ -1457,7 +1486,7 @@ export class RestApi implements Api { formData.append("systemIds", serviceIds.join(",")); } const response = await this.instance.post( - `${this.v1()}/catalog/context-system/export`, + `/api/v1/${getAppName()}/catalog/context-system/export`, formData, { headers: { @@ -1472,7 +1501,7 @@ export class RestApi implements Api { getService = async (id: string): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/systems/${id}`, + `/api/v1/${getAppName()}/systems-catalog/systems/${id}`, ); return response.data; }; @@ -1482,7 +1511,7 @@ export class RestApi implements Api { data: Partial, ): Promise => { const response = await this.instance.put( - `${this.v1()}/systems-catalog/systems/${id}`, + `/api/v1/${getAppName()}/systems-catalog/systems/${id}`, data, ); return response.data; @@ -1490,7 +1519,7 @@ export class RestApi implements Api { getEnvironments = async (systemId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/systems/${systemId}/environments`, + `/api/v1/${getAppName()}/systems-catalog/systems/${systemId}/environments`, ); return response.data; }; @@ -1499,7 +1528,7 @@ export class RestApi implements Api { systemId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/specificationGroups`, + `/api/v1/${getAppName()}/systems-catalog/specificationGroups`, { params: { systemId: systemId, @@ -1513,7 +1542,7 @@ export class RestApi implements Api { systemId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/models/latest`, + `/api/v1/${getAppName()}/systems-catalog/models/latest`, { params: { systemId: systemId, @@ -1528,7 +1557,7 @@ export class RestApi implements Api { data: Partial, ): Promise => { const response = await this.instance.patch( - `${this.v1()}/systems-catalog/specificationGroups/${id}`, + `/api/v1/${getAppName()}/systems-catalog/specificationGroups/${id}`, data, ); return response.data; @@ -1536,7 +1565,7 @@ export class RestApi implements Api { deleteSpecificationGroup = async (id: string): Promise => { await this.instance.delete( - `${this.v1()}/systems-catalog/specificationGroups/${id}`, + `/api/v1/${getAppName()}/systems-catalog/specificationGroups/${id}`, ); }; @@ -1545,7 +1574,7 @@ export class RestApi implements Api { data: Partial, ): Promise => { const response = await this.instance.patch( - `${this.v1()}/systems-catalog/models/${id}`, + `/api/v1/${getAppName()}/systems-catalog/models/${id}`, data, ); return response.data; @@ -1553,13 +1582,15 @@ export class RestApi implements Api { getSpecificationModelSource = async (id: string): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/models/${id}/source`, + `/api/v1/${getAppName()}/systems-catalog/models/${id}/source`, ); return response.data; }; deleteSpecificationModel = async (id: string): Promise => { - await this.instance.delete(`${this.v1()}/systems-catalog/models/${id}`); + await this.instance.delete( + `/api/v1/${getAppName()}/systems-catalog/models/${id}`, + ); }; getSpecificationModel = async ( @@ -1567,7 +1598,7 @@ export class RestApi implements Api { specificationGroupId?: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/models`, + `/api/v1/${getAppName()}/systems-catalog/models`, { params: { systemId: systemId, @@ -1580,7 +1611,7 @@ export class RestApi implements Api { deprecateModel = async (modelId: string): Promise => { const response = await this.instance.post( - `${this.v1()}/systems-catalog/models/deprecated`, + `/api/v1/${getAppName()}/systems-catalog/models/deprecated`, modelId, { headers: { "Content-Type": "text/plain" }, @@ -1591,7 +1622,7 @@ export class RestApi implements Api { getOperations = async (modelId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/operations`, + `/api/v1/${getAppName()}/systems-catalog/operations`, { params: { modelId, @@ -1603,7 +1634,7 @@ export class RestApi implements Api { getOperationInfo = async (operationId: string): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/operations/${encodeURIComponent(operationId)}/info`, + `/api/v1/${getAppName()}/systems-catalog/operations/${encodeURIComponent(operationId)}/info`, ); return response.data; }; @@ -1634,8 +1665,8 @@ export class RestApi implements Api { const url = systemType === IntegrationSystemType.CONTEXT - ? `${this.v1()}/catalog/context-system/import` - : `${this.v1()}/systems-catalog/import/system`; + ? `/api/v1/${getAppName()}/catalog/context-system/import` + : `/api/v1/${getAppName()}/systems-catalog/import/system`; const response = await this.instance.post( url, formData, @@ -1659,7 +1690,7 @@ export class RestApi implements Api { specificationGroupId: specificationGroupId, }; const response = await this.instance.post( - `${this.v1()}/systems-catalog/import`, + `/api/v1/${getAppName()}/systems-catalog/import`, formData, { params, @@ -1683,7 +1714,7 @@ export class RestApi implements Api { }; if (protocol) params.protocol = protocol; const response = await this.instance.post( - `${this.v1()}/systems-catalog/specificationGroups/import`, + `/api/v1/${getAppName()}/systems-catalog/specificationGroups/import`, formData, { params, @@ -1697,7 +1728,7 @@ export class RestApi implements Api { importId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/systems-catalog/import/${importId}`, + `/api/v1/${getAppName()}/systems-catalog/import/${importId}`, ); return response.data; }; @@ -1706,7 +1737,7 @@ export class RestApi implements Api { includeContent: boolean, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/detailed-design/templates`, + `/api/v1/${getAppName()}/catalog/detailed-design/templates`, { params: { includeContent, @@ -1720,7 +1751,7 @@ export class RestApi implements Api { templateId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/detailed-design/templates/${templateId}`, + `/api/v1/${getAppName()}/catalog/detailed-design/templates/${templateId}`, ); return response.data; }; @@ -1730,7 +1761,7 @@ export class RestApi implements Api { content: string, ): Promise => { const response = await this.instance.put( - `${this.v1()}/catalog/detailed-design/templates`, + `/api/v1/${getAppName()}/catalog/detailed-design/templates`, { name, content }, ); return response.data; @@ -1738,7 +1769,7 @@ export class RestApi implements Api { deleteDetailedDesignTemplates = async (ids: string[]): Promise => { await this.instance.delete( - `${this.v1()}/catalog/detailed-design/templates`, + `/api/v1/${getAppName()}/catalog/detailed-design/templates`, { params: { ids, @@ -1752,7 +1783,7 @@ export class RestApi implements Api { templateId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/detailed-design/chains/${chainId}`, + `/api/v1/${getAppName()}/catalog/detailed-design/chains/${chainId}`, { params: { templateId, @@ -1767,7 +1798,7 @@ export class RestApi implements Api { diagramModes: DiagramMode[], ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/design-generator/chains/${chainId}`, + `/api/v1/${getAppName()}/catalog/design-generator/chains/${chainId}`, { diagramModes, }, @@ -1781,7 +1812,7 @@ export class RestApi implements Api { diagramModes: DiagramMode[], ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/design-generator/chains/${chainId}/snapshots/${snapshotId}`, + `/api/v1/${getAppName()}/catalog/design-generator/chains/${chainId}/snapshots/${snapshotId}`, { diagramModes, }, @@ -1806,7 +1837,7 @@ export class RestApi implements Api { elementIds: string[], ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/chains/${chainId}/elements/groups`, + `/api/v1/${getAppName()}/catalog/chains/${chainId}/elements/groups`, elementIds, ); return response.data; @@ -1817,14 +1848,14 @@ export class RestApi implements Api { groupId: string, ): Promise => { const response = await this.instance.delete( - `${this.v1()}/catalog/chains/${chainId}/elements/groups/${groupId}`, + `/api/v1/${getAppName()}/catalog/chains/${chainId}/elements/groups/${groupId}`, ); return response.data; }; getExchanges = async (limit: number): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/live-exchanges`, + `/api/v1/${getAppName()}/catalog/live-exchanges`, { params: { limit: limit, @@ -1840,7 +1871,7 @@ export class RestApi implements Api { exchangeId: string, ): Promise => { await this.instance.delete( - `${this.v1()}/catalog/live-exchanges/${podIp}/${deploymentId}/${exchangeId}`, + `/api/v1/${getAppName()}/catalog/live-exchanges/${podIp}/${deploymentId}/${exchangeId}`, ); }; @@ -1853,7 +1884,7 @@ export class RestApi implements Api { searchString: string, ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/diagnostic/validations`, + `/api/v1/${getAppName()}/catalog/diagnostic/validations`, { searchString, filters }, ); return response.data; @@ -1863,14 +1894,14 @@ export class RestApi implements Api { validationId: string, ): Promise => { const response = await this.instance.get( - `${this.v1()}/catalog/diagnostic/validations/${validationId}`, + `/api/v1/${getAppName()}/catalog/diagnostic/validations/${validationId}`, ); return response.data; }; runValidations = async (ids: string[]): Promise => { await this.instance.patch( - `${this.v1()}/catalog/diagnostic/validations`, + `/api/v1/${getAppName()}/catalog/diagnostic/validations`, undefined, { params: { validationIds: ids }, @@ -1882,7 +1913,7 @@ export class RestApi implements Api { request: BulkDeploymentRequest, ): Promise => { const response = await this.instance.post( - `${this.v1()}/catalog/chains/deployments/bulk`, + `/api/v1/${getAppName()}/catalog/chains/deployments/bulk`, request, ); return response.data; diff --git a/src/api/rest/vscodeExtensionApi.ts b/src/api/rest/vscodeExtensionApi.ts index e4dce472..9b796229 100644 --- a/src/api/rest/vscodeExtensionApi.ts +++ b/src/api/rest/vscodeExtensionApi.ts @@ -55,6 +55,7 @@ import { BulkDeploymentResult, ImportVariablesResult, VariableImportPreview, + UsedProperty } from "../apiTypes.ts"; import { Api } from "../api.ts"; import { getAppName } from "../../appConfig.ts"; @@ -694,10 +695,22 @@ export class VSCodeExtensionApi implements Api { throw new Error("Method loadCatalogActionsLog not implemented."); } + loadVariablesManagementActionsLog(): Promise { + throw new Error( + "Method loadVariablesManagementActionsLog not implemented.", + ); + } + exportCatalogActionsLog(): Promise { throw new Error("Method exportCatalogActionsLog not implemented."); } + exportVariablesManagementActionsLog(): Promise { + throw new Error( + "Method exportVariablesManagementActionsLog not implemented.", + ); + } + getChains(): Promise { throw new Error("Method getChains not implemented."); } @@ -1006,6 +1019,10 @@ export class VSCodeExtensionApi implements Api { downloadHelmChart(_secretName: string): Promise { throw new RestApiError("Not implemented", 501); } + + getUsedProperties(chainId: string): Promise { + throw new Error("Method loadHttpTriggerAccessControl not implemented."); + } } interface VSCodeApi { diff --git a/src/components/UsedPropertiesList.module.css b/src/components/UsedPropertiesList.module.css new file mode 100644 index 00000000..e292739b --- /dev/null +++ b/src/components/UsedPropertiesList.module.css @@ -0,0 +1,140 @@ +.usedPropertiesTree { + padding: 8px; + color: var(--vscode-foreground, rgba(0, 0, 0, 0.88)); +} + +.propertyItem { + margin-bottom: 2px; +} + +.menuItemContainer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + cursor: pointer; + user-select: none; + border-radius: 4px; + transition: background-color 0.2s; +} + +.menuItemContainer:hover { + background-color: var(--vscode-list-hoverBackground, rgba(0, 0, 0, 0.04)); +} + +.propertyRow { + font-weight: 500; +} + +.leftContent { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +} + +.rightContent { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.propertySource { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 3px; + background-color: var(--vscode-badge-background, #e5e5e5); + color: var(--vscode-badge-foreground, #4a4a4a); + font-size: 11px; + font-weight: 600; + flex-shrink: 0; +} + +.propertyName { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.propertyType { + font-size: 11px; + color: var(--vscode-descriptionForeground, rgba(0, 0, 0, 0.45)); + font-style: italic; +} + +.propertyChildrenCount { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 18px; + padding: 0 6px; + border-radius: 10px; + background-color: var(--vscode-badge-background, #e5e5e5); + color: var(--vscode-badge-foreground, #4a4a4a); + font-size: 11px; + font-weight: 500; +} + +.expandIcon { + font-size: 10px; + color: #4a4a4a !important; + width: 16px; + text-align: center; +} + +.elementsContainer { + margin-left: 28px; + border-left: 1px solid var(--vscode-border, rgba(0, 0, 0, 0.15)); + padding-left: 8px; +} + +.elementRow { + font-size: 13px; + padding: 4px 12px; +} + +.elementInfo { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.elementName { + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.elementType { + font-size: 11px; + color: var(--vscode-descriptionForeground, rgba(0, 0, 0, 0.45)); +} + +.operationChip { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; +} + +.operationGreen { + background-color: #d4edda; + color: #155724; +} + +.operationBlue { + background-color: #d1ecf1; + color: #0c5460; +} diff --git a/src/components/UsedPropertiesList.tsx b/src/components/UsedPropertiesList.tsx new file mode 100644 index 00000000..f1df6c42 --- /dev/null +++ b/src/components/UsedPropertiesList.tsx @@ -0,0 +1,238 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { Spin, Empty } from 'antd'; +import { useUsedProperties } from "../hooks/useUsedProperties.tsx"; +import { UsedProperty } from "../api/apiTypes.ts"; +import { useLibraryContext } from "./LibraryContext.tsx"; +import { OverridableIcon, IconName } from "../icons/IconProvider.tsx"; +import styles from "./UsedPropertiesList.module.css"; + + +const USED_PROPERTY_SOURCE_LABEL_MAPPING: { [key: string]: string } = { + HEADER: 'H', + EXCHANGE_PROPERTY: 'P', +}; + +const USED_PROPERTY_TYPE_LABEL_MAPPING: { [key: string]: string } = { + STRING: 'string', + NUMBER: 'number', + BOOLEAN: 'boolean', + OBJECT: 'object', + UNKNOWN_TYPE: '—', +}; + +const usedPropertyElementOperationColorMapping: { [key: string]: 'green' | 'blue' } = { + GET: 'green', + SET: 'blue', +}; + +interface ParsedProperty { + id: string; + name: string; + source: string; + sourceCode: string; + type: string; + isArray: boolean; + childrenCount: number; + children: ParsedElement[]; +} + +interface ParsedElement { + id: string; + elementId: string; + name: string; + type: string; + typeTitle: string; + operations: Array<{ + operation: 'GET' | 'SET'; + operationColor: 'green' | 'blue'; + }>; +} + +interface UsedPropertiesListProps { + chainId: string; + onElementSingleClick?: (elementId: string) => void; + onElementDoubleClick?: (elementId: string) => void; +} + +export const UsedPropertiesList: React.FC = ({ + chainId, + onElementSingleClick, + onElementDoubleClick, +}) => { + const { properties, isLoading } = useUsedProperties(chainId); + const { libraryElements } = useLibraryContext(); + + const [expandedProperties, setExpandedProperties] = useState>(new Set()); + + const getElementTemplate = useCallback((type: string): { title: string } | null => { + if (!libraryElements) return null; + const libraryElement = libraryElements.find(el => el.name === type); + return libraryElement ? { title: libraryElement.title } : null; + }, [libraryElements]); + + const buildUsedPropMapKey = (property: UsedProperty): string => { + return property.name + property.source; + }; + + const buildUsedElementMapKey = ( + propertyName: string, + propertySource: string, + elementId: string + ): string => { + return propertyName + propertySource + elementId; + }; + + const parsedProperties = useMemo(() => { + if (!properties || properties.length === 0) { + return []; + } + + const sortedProperties = [...properties].sort((a, b) => + a.name.localeCompare(b.name) + ); + + return sortedProperties.map(property => { + const propertyId = buildUsedPropMapKey(property); + + const children: ParsedElement[] = Object.values(property.relatedElements) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(element => { + const elementDescriptor = getElementTemplate(element.type); + + return { + id: buildUsedElementMapKey(property.name, property.source, element.id), + elementId: element.id, + name: element.name, + type: element.type, + typeTitle: elementDescriptor?.title || element.type, + operations: element.operations.map(op => ({ + operation: op, + operationColor: usedPropertyElementOperationColorMapping[op], + })), + }; + }); + + return { + id: propertyId, + name: property.name, + source: property.source, + sourceCode: USED_PROPERTY_SOURCE_LABEL_MAPPING[property.source], + type: USED_PROPERTY_TYPE_LABEL_MAPPING[property.type], + isArray: property.isArray, + childrenCount: Object.values(property.relatedElements).length, + children, + }; + }); + }, [properties, getElementTemplate]); + + const toggleProperty = (propertyId: string) => { + setExpandedProperties(prev => { + const next = new Set(prev); + if (next.has(propertyId)) { + next.delete(propertyId); + } else { + next.add(propertyId); + } + return next; + }); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (parsedProperties.length === 0) { + return ( + + ); + } + + return ( +
+ {parsedProperties.map(property => { + const isExpanded = expandedProperties.has(property.id); + + return ( +
+ {/* Level 0: Property */} +
toggleProperty(property.id)} + > +
+ {property.sourceCode} + {property.name} +
+
+ + [{property.isArray ? 'array of ' : ''}{property.type}] + + + {property.childrenCount > 99 ? '99+' : property.childrenCount} + + + + +
+
+ + {isExpanded && ( +
+ {property.children.map(element => ( +
{ + e.stopPropagation(); + onElementSingleClick?.(element.elementId); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + onElementDoubleClick?.(element.elementId); + }} + > +
+ +
+
{element.name}
+ {element.typeTitle} +
+
+
+ {element.operations.map((op, idx) => { + const colorClass = op.operationColor === 'green' + ? styles.operationGreen + : styles.operationBlue; + return ( + + {op.operation} + + ); + })} +
+
+ ))} +
+ )} +
+ ); + })} +
+ ); +}; diff --git a/src/components/elements_library/ElementsLibrarySidebar.module.css b/src/components/elements_library/ElementsLibrarySidebar.module.css index efcd295a..286d0a5e 100644 --- a/src/components/elements_library/ElementsLibrarySidebar.module.css +++ b/src/components/elements_library/ElementsLibrarySidebar.module.css @@ -45,3 +45,32 @@ var(--vscode-foreground, rgb(0 0 0 / 88%)) ) !important; } + +.spacedTabs :global(.ant-tabs-nav-list) { + width: 100%; + display: flex; + justify-content: flex-start; + margin-left: 20px; +} + +.spacedTabs :global(.ant-tabs-tab) { + position: relative; +} + +.spacedTabs { + width: 100%; + margin-bottom: 10px; +} + +.spacedTabs :global(.ant-tabs-nav) { + width: 100%; + margin: 0; + padding: 1px; + border-top: 1px solid var(--vscode-border, rgba(0, 0, 0, 0.15)) !important; +} + +.spacedTabs :global(.ant-tabs-nav::before) { + width: 100% !important; + left: 0 !important; +} + diff --git a/src/components/graph/CustomControls.tsx b/src/components/graph/CustomControls.tsx index a62f7ccb..3868c8f8 100644 --- a/src/components/graph/CustomControls.tsx +++ b/src/components/graph/CustomControls.tsx @@ -7,7 +7,7 @@ import { OverridableIcon } from "../../icons/IconProvider.tsx"; export const CustomControls = () => { const { zoomIn, zoomOut, fitView } = useReactFlow(); - const { toggleDirection } = useElkDirectionContext(); + const { toggleDirection, toggleRightPanel } = useElkDirectionContext(); return (
@@ -41,6 +41,13 @@ export const CustomControls = () => { onClick={toggleDirection} icon={} /> +
); }; diff --git a/src/hooks/graph/useElkDirection.tsx b/src/hooks/graph/useElkDirection.tsx index 4e6105f6..23917ca9 100644 --- a/src/hooks/graph/useElkDirection.tsx +++ b/src/hooks/graph/useElkDirection.tsx @@ -4,10 +4,15 @@ export type ElkDirection = "RIGHT" | "DOWN"; export const useElkDirection = () => { const [direction, setDirection] = useState("RIGHT"); + const [rightPanel, setRightPanel] = useState(false); const toggleDirection = useCallback(() => { setDirection((d) => (d === "RIGHT" ? "DOWN" : "RIGHT")); }, []); - return { direction, toggleDirection }; + const toggleRightPanel = useCallback(() => { + setRightPanel((prev) => !prev); + }, []); + + return { direction, toggleDirection, rightPanel, toggleRightPanel }; }; diff --git a/src/hooks/useUsedProperties.tsx b/src/hooks/useUsedProperties.tsx new file mode 100644 index 00000000..ceff2fdb --- /dev/null +++ b/src/hooks/useUsedProperties.tsx @@ -0,0 +1,41 @@ +import { api } from "../api/api.ts"; +import { + UsedProperty +} from "../api/apiTypes.ts"; +import {useCallback, useEffect, useState} from "react"; +import { useNotificationService } from "./useNotificationService.tsx"; + +export const useUsedProperties = ( + chainId: string, +) => { + const [isLoading, setIsLoading] = useState(false); + const [properties, setProperties] = useState([]); + const notificationService = useNotificationService(); + + const getUsedProperties = useCallback( + async () => { + try { + setIsLoading(true); + const responseData = await api.getUsedProperties(chainId); + setProperties(responseData); + } catch (error) { + notificationService.requestFailed("Failed to load Used Properties", error); + } finally { + setIsLoading(false); + } + }, + [chainId, notificationService], + ); + + useEffect(() => { + if (chainId) { + void getUsedProperties(); + } + }, [chainId, getUsedProperties]); + + return { + properties, + isLoading, + refresh: getUsedProperties, + }; +}; diff --git a/src/icons/IconDefenitions.tsx b/src/icons/IconDefenitions.tsx index 980c9097..adc3e61b 100644 --- a/src/icons/IconDefenitions.tsx +++ b/src/icons/IconDefenitions.tsx @@ -105,6 +105,8 @@ import { BulbOutlined, BarChartOutlined, ToolOutlined, + RightSquareOutlined, + MenuUnfoldOutlined } from "@ant-design/icons"; export const commonIcons = { @@ -185,6 +187,9 @@ export const commonIcons = { bulb: BulbOutlined, barChart: BarChartOutlined, tool: ToolOutlined, + rightPanel: RightSquareOutlined, + block: BlockOutlined, + menuUnfold: MenuUnfoldOutlined }; export const elementIcons = { diff --git a/src/pages/ChainGraph.tsx b/src/pages/ChainGraph.tsx index 14129d58..713f3db2 100644 --- a/src/pages/ChainGraph.tsx +++ b/src/pages/ChainGraph.tsx @@ -37,6 +37,8 @@ import { import { useChainGraph } from "../hooks/graph/useChainGraph.tsx"; import { ElkDirectionContextProvider } from "./ElkDirectionContext.tsx"; +import { useElkDirection } from "../hooks/graph/useElkDirection.tsx"; +import { PageWithRightPanel } from "./PageWithRightPanel.tsx"; import { SaveAndDeploy } from "../components/modal/SaveAndDeploy.tsx"; import { CreateDeploymentRequest, Element } from "../api/apiTypes.ts"; import { api } from "../api/api.ts"; @@ -129,6 +131,8 @@ const ChainGraphInner: React.FC = () => { isLoading, } = useChainGraph(chainId, refreshChain); + const { rightPanel, toggleRightPanel } = useElkDirection(); + const handleElementUpdated = useCallback( (element: Element, node: ChainGraphNode) => { updateNodeData(element, node); @@ -468,7 +472,7 @@ const ChainGraphInner: React.FC = () => {
{
+ {rightPanel && } }> ⭾} diff --git a/src/pages/PageWithRightPanel.tsx b/src/pages/PageWithRightPanel.tsx new file mode 100644 index 00000000..64a54e73 --- /dev/null +++ b/src/pages/PageWithRightPanel.tsx @@ -0,0 +1,254 @@ +import Sider from "antd/lib/layout/Sider"; +import styles from "../components/elements_library/ElementsLibrarySidebar.module.css"; +import {SidebarSearch} from "../components/elements_library/SidebarSearch.tsx"; +import {useCallback, useRef, useState, useMemo, useEffect} from "react"; +import {MenuItem} from "../components/elements_library/ElementsLibrarySidebar.tsx"; +import {Flex, Menu, Tabs} from "antd"; +import {OverridableIcon, IconName} from "../icons/IconProvider.tsx"; +import { FilterButton } from "../components/table/filter/FilterButton.tsx"; +import {useChainFilters} from "../hooks/useChainFilter.ts"; +import {Filter} from "../components/table/filter/Filter.tsx"; +import {FilterItemState} from "../components/table/filter/FilterItem.tsx"; +import {Element} from "../api/apiTypes.ts"; +import {useModalsContext} from "../Modals.tsx"; +import {useParams, useNavigate} from "react-router-dom"; +import {useLibraryContext} from "../components/LibraryContext.tsx"; +import {getLibraryElement, getNodeFromElement} from "../misc/chain-graph-utils.ts"; +import type {MenuProps} from "antd"; +import {api} from "../api/api.ts"; +import {useNotificationService} from "../hooks/useNotificationService.tsx"; +import {ChainContext} from "./ChainPage.tsx"; +import {useContext} from "react"; +import {ChainElementModification} from "../components/modal/chain_element/ChainElementModification.tsx"; +import {ChainGraphNode} from "../components/graph/nodes/ChainGraphNodeTypes.ts"; +import {useElkDirectionContext} from "./ElkDirectionContext.tsx"; +import {UsedPropertiesList} from "../components/UsedPropertiesList.tsx"; +import { isVsCode } from "../api/rest/vscodeExtensionApi.ts"; +import {EntityFilterModel} from "../components/table/filter/filter.ts"; + +export const PageWithRightPanel = () => { + const allItems = useRef([]); + const [isSearch, setIsSearch] = useState(false); + const { filterColumns, filterItemStates, setFilterItemStates } = + useChainFilters(); + const [openKeysState, setOpenKeysState] = useState(); + const openKeysBeforeSearch = useRef(); + const [items, setItems] = useState([]); + const { showModal } = useModalsContext(); + const [filters, setFilters] = useState([]); + const [activeTab, setActiveTab] = useState("listElements"); + + const { chainId } = useParams(); + const { libraryElements } = useLibraryContext(); + const notificationService = useNotificationService(); + const navigate = useNavigate(); + const chainContext = useContext(ChainContext); + const [elements, setElements] = useState([]); + + let direction: "RIGHT" | "DOWN" = "RIGHT"; + try { + const elkContext = useElkDirectionContext(); + direction = elkContext.direction; + } catch {} + + useEffect(() => { + if (!chainId) { + setElements([]); + return; + } + + const fetchElements = async () => { + try { + const fetchedElements = await api.getElements(chainId); + setElements(fetchedElements); + } catch (error) { + notificationService.requestFailed("Failed to load elements", error); + } + }; + + void fetchElements(); + + const handleFocus = () => { + void fetchElements(); + }; + window.addEventListener('focus', handleFocus); + + const intervalId = setInterval(() => { + void fetchElements(); + }, 3000); + + return () => { + clearInterval(intervalId); + window.removeEventListener('focus', handleFocus); + }; + }, [chainId, notificationService]); + + const handleElementDoubleClick = useCallback((element: Element) => { + if (!chainId || !chainContext) return; + + const libraryElement = getLibraryElement(element, libraryElements); + const node: ChainGraphNode = getNodeFromElement( + element, + libraryElement, + direction + ); + + const modalId = `chain-element-${element.id}`; + showModal({ + id: modalId, + component: ( + + { + // Element was updated - refresh the list + void api.getElements(chainId).then(setElements); + }} + onClose={() => { + if (chainId) { + navigate(`/chains/${chainId}/graph`); + } + }} + /> + + ), + }); + }, [chainId, chainContext, libraryElements, direction, showModal, navigate]); + + const handleElementDoubleClickById = useCallback((elementId: string) => { + const element = elements.find(el => el.id === elementId); + if (element) { + handleElementDoubleClick(element); + } + }, [elements, handleElementDoubleClick]); + + const handleElementSingleClick = useCallback((elementId: string) => {}, []); + + const elementMenuItems: MenuProps['items'] = useMemo(() => { + if (!elements?.length || !libraryElements) { + return []; + } + + return elements.map((element: Element) => { + const libraryElement = getLibraryElement(element, libraryElements); + const elementName = element.name || libraryElement.title || element.type; + return { + key: element.id, + label: ( +
{ + e.stopPropagation(); + handleElementDoubleClick(element); + }} + style={{ cursor: 'pointer' }} + > + + {elementName} +
+ ), + title: `${elementName} (${libraryElement.title || element.type})`, + }; + }); + }, [elements, libraryElements, handleElementDoubleClick]); + + const handleSearch = useCallback( + (filtered: MenuItem[], openKeys: string[]) => { + if (!isSearch) { + setIsSearch(true); + openKeysBeforeSearch.current = openKeysState; + } + setOpenKeysState(openKeys); + setItems(filtered); + + }, + [isSearch, openKeysState], + ); + + const applyFilters = (filterItems: FilterItemState[]) => { + setFilterItemStates?.(filterItems); + + const f = (filterItems ?? []).map( + (filterItem): EntityFilterModel => ({ + column: filterItem.columnValue!, + condition: filterItem.conditionValue!, + value: filterItem.value, + }), + ); + setFilters(f); + }; + + const addFilter = () => { + showModal({ + component: ( + + ), + }); + }; + + return ( + + + + }, + ...(!isVsCode ? [{ + key: "elementProperties", + label: + }] : []), + ]} + > + + + + { + setItems(allItems.current); + setIsSearch(false); + setOpenKeysState(openKeysBeforeSearch.current); + }} + /> + + + {activeTab === "listElements" && ( + + )} + {activeTab === "elementProperties" && chainId && ( + + )} + {activeTab === "elementProperties" && !chainId && ( +
+ No chain selected +
+ )} + + + ); +};