From 4b845d400a1333a0af80973748b5cec78014ba08 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:47:11 +0300 Subject: [PATCH 01/19] feat: added button that exports selected chains as Camel K custom resource --- src/api/api.ts | 3 +++ src/api/apiTypes.ts | 9 ++++++++ src/api/rest/restApi.ts | 11 +++++++++ src/api/rest/vscodeExtensionApi.ts | 9 ++++++-- src/pages/Chains.tsx | 36 +++++++++++++++++++++++++++--- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index 76f0edf6..56e5b6a8 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -58,6 +58,7 @@ import { TransferElementRequest, Element, SystemOperation, + CustomResourceBuildRequest, } from "./apiTypes.ts"; import { RestApi } from "./rest/restApi.ts"; import { isVsCode, VSCodeExtensionApi } from "./rest/vscodeExtensionApi.ts"; @@ -147,6 +148,8 @@ export interface Api { revertToSnapshot(chainId: string, snapshotId: string): Promise; + buildCR(request: CustomResourceBuildRequest): Promise; + getLibraryElementByType(type: string): Promise; getDeployments(chainId: string): Promise; diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index c6a697dd..c03f9fb2 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -1126,3 +1126,12 @@ export enum ApiSpecificationFormat { YAML = "YAML", } +export type CustomResourceBuildRequest = { + options: CustomResourceOptions; + chainIds: string[]; +} + +export type CustomResourceOptions = { + language: string; + image: string; +}; diff --git a/src/api/rest/restApi.ts b/src/api/rest/restApi.ts index 1808c172..234c4a5e 100644 --- a/src/api/rest/restApi.ts +++ b/src/api/rest/restApi.ts @@ -65,6 +65,7 @@ import { TransferElementRequest, Element, SystemOperation, + CustomResourceBuildRequest, } from "../apiTypes.ts"; import { Api } from "../api.ts"; import { getFileFromResponse } from "../../misc/download-utils.ts"; @@ -1323,4 +1324,14 @@ export class RestApi implements Api { ); return response.data; }; + + buildCR = async ( + request: CustomResourceBuildRequest + ): Promise => { + const response = await this.instance.post( + `/api/v1/${getAppName()}/catalog/cr`, + request, + ); + return response.data; + }; } diff --git a/src/api/rest/vscodeExtensionApi.ts b/src/api/rest/vscodeExtensionApi.ts index 5deb548c..e9244c9f 100644 --- a/src/api/rest/vscodeExtensionApi.ts +++ b/src/api/rest/vscodeExtensionApi.ts @@ -44,8 +44,9 @@ import { SystemRequest, UsedService, Element, - MaskedFields, TransferElementRequest, - SystemOperation + MaskedFields, + TransferElementRequest, + SystemOperation, } from "../apiTypes.ts"; import { Api } from "../api.ts"; import { getAppName } from "../../appConfig.ts"; @@ -691,6 +692,10 @@ export class VSCodeExtensionApi implements Api { moveFolder(): Promise { throw new Error("Method moveFolder not implemented."); } + + buildCR(): Promise { + throw new Error("Method buildCR not implemented."); + } } interface VSCodeApi { diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index c927544c..767a8f0c 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -14,17 +14,18 @@ import { useModalsContext } from "../Modals.tsx"; import { CatalogItemType, ChainCreationRequest, - ChainItem, + ChainItem, CustomResourceBuildRequest, FolderFilter, FolderItem, ListFolderRequest, - UpdateFolderRequest, + UpdateFolderRequest } from "../api/apiTypes.ts"; +import { KubernetesOutlined } from "@ant-design/icons"; import React, { useCallback, useEffect, useState } from "react"; import { api } from "../api/api.ts"; import { TableProps } from "antd/lib/table"; import { TextColumnFilterDropdown } from "../components/table/TextColumnFilterDropdown.tsx"; -import { formatTimestamp } from "../misc/format-utils.ts"; +import { formatDate, formatTimestamp } from "../misc/format-utils.ts"; import { TimestampColumnFilterDropdown } from "../components/table/TimestampColumnFilterDropdown.tsx"; import { EntityLabels } from "../components/labels/EntityLabels.tsx"; import { TableRowSelection } from "antd/lib/table/interface"; @@ -428,6 +429,30 @@ const Chains = () => { } }; + const exportCR = async () => { + const ids = selectedRowKeys.map((k) => k.toString()); + setIsLoading(true); + try { + const request: CustomResourceBuildRequest = { + options: { + language: "xml", + image: "qip-engine", + }, + chainIds: ids + }; + const text = await api.buildCR(request); + const blob = new Blob([text], { type: "application/yaml" }); + const timestamp = formatDate(new Date()); + const fileName = `cr-${timestamp}.yaml`; + const file = new File([blob], fileName, { type: "application/yaml" }); + downloadFile(file); + } catch (error) { + notificationService.requestFailed("Failed to export CR", error); + } finally { + setIsLoading(false); + } + } + const pasteItem = async (destinationFolderId?: string) => { if (!operation) { return; @@ -975,6 +1000,11 @@ const Chains = () => { icon={} // onClick={onDeployBtnClick} /> + } + onClick={() => void exportCR()} + /> } From 8b48bf39ddb2a118bdbe365f3bf23e3bde8d1275 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:26:46 +0300 Subject: [PATCH 02/19] feat: added custom resource generation dialog --- src/api/apiTypes.ts | 11 +- src/components/modal/ExportCRDialog.tsx | 373 ++++++++++++++++++++++++ src/pages/Chains.tsx | 18 +- 3 files changed, 393 insertions(+), 9 deletions(-) create mode 100644 src/components/modal/ExportCRDialog.tsx diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index c03f9fb2..6547c3dd 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -1132,6 +1132,13 @@ export type CustomResourceBuildRequest = { } export type CustomResourceOptions = { - language: string; - image: string; + language?: string; + name?: string; + container?: ContainerOptions; + environment?: Record; }; + +export type ContainerOptions = { + image?: string; + imagePoolPolicy?: "Always" | "Never" | "IfNotPresent"; +} diff --git a/src/components/modal/ExportCRDialog.tsx b/src/components/modal/ExportCRDialog.tsx new file mode 100644 index 00000000..8db7e984 --- /dev/null +++ b/src/components/modal/ExportCRDialog.tsx @@ -0,0 +1,373 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { CustomResourceOptions } from "../../api/apiTypes.ts"; +import { + Button, + Flex, + Form, + Input, + message, + Modal, + Select, + Table, + TableProps, + Tabs, +} from "antd"; +import { useModalContext } from "../../ModalContextProvider.tsx"; +import { useNotificationService } from "../../hooks/useNotificationService.tsx"; +import { formatDate } from "../../misc/format-utils.ts"; +import { Icon } from "../../IconProvider.tsx"; +import { InlineEdit } from "../InlineEdit.tsx"; +import { TextValueEdit } from "../table/TextValueEdit.tsx"; + +type EnvironmentEditorProps = { + value?: Record; + onChange?: (value: Record) => void; +}; + +type NameValuePair = { + name: string; + value: string; +}; + +function makeRecord(pairs: NameValuePair[]): Record { + const result: Record = {}; + pairs.forEach((pair) => (result[pair.name] = pair.value)); + return result; +} + +function removeDuplicateKeys(data: NameValuePair[]): NameValuePair[] { + const seenKeys = new Set(); + return data.filter((item) => { + if (seenKeys.has(item.name)) { + return false; + } else { + seenKeys.add(item.name); + return true; + } + }); +} + +const EnvironmentEditor: React.FC = ({ + onChange, + value, +}) => { + const [messageApi, contextHolder] = message.useMessage(); + const [tableData, setTableData] = useState< + TableProps["dataSource"] + >([]); + + useEffect(() => { + try { + const items: NameValuePair[] = Object.entries(value ?? {}).map( + ([name, value]) => ({ name, value }), + ); + setTableData(removeDuplicateKeys(items)); + } catch (error) { + void messageApi.error(`Invalid dictionary: ${String(error)}`); + setTableData([]); + } + }, [messageApi, value]); + + const updateRecord = useCallback( + (index: number, changes: Partial) => { + setTableData((data) => { + const result = + data?.map((r, idx) => (idx === index ? { ...r, ...changes } : r)) ?? + []; + onChange?.(makeRecord(result)); + return result; + }); + }, + [onChange], + ); + + const addRecord = useCallback(() => { + setTableData((data) => { + if (data?.some((r) => r.name === "")) { + return data; + } + const result = [...(data ?? []), { name: "", value: "" }]; + onChange?.(makeRecord(result)); + return result; + }); + }, [onChange]); + + const deleteRecord = useCallback( + (index: number) => { + setTableData((data) => { + const result = + data?.slice(0, index)?.concat(data?.slice(index + 1)) ?? []; + onChange?.(makeRecord(result)); + return result; + }); + }, + [onChange], + ); + + const clearRecords = useCallback(() => { + setTableData([]); + onChange?.(makeRecord([])); + }, [onChange]); + + return ( + <> + {contextHolder} + + + + + + { + if (sortOrder === "ascend") { + return a.name.localeCompare(b.name); + } else if (sortOrder === "descend") { + return b.name.localeCompare(a.name); + } + return 0; + }, + render: ( + value: string, + _record: NameValuePair, + index: number, + ) => { + return ( + + values={{ value }} + editor={} + viewer={value} + initialActive={value === ""} + onSubmit={({ value }) => { + if (tableData?.some((r) => r.name === value)) { + void messageApi.error(`Already exists: ${value}`); + } else { + updateRecord(index, { name: value }); + } + }} + /> + ); + }, + }, + { + key: "value", + title: "Value", + dataIndex: "value", + sorter: (a, b, sortOrder) => { + if (sortOrder === "ascend") { + return a.value.localeCompare(b.value); + } else if (sortOrder === "descend") { + return b.value.localeCompare(a.value); + } + return 0; + }, + render: ( + value: string, + _record: NameValuePair, + index: number, + ) => { + return ( + + values={{ value }} + editor={} + viewer={value} + onSubmit={({ value }) => { + updateRecord(index, { value }); + }} + /> + ); + }, + }, + { + key: "actions", + className: "actions-column", + width: 40, + align: "right", + render: (_, _record, index: number) => { + return ( + , + , + ]} + > + + id="crOptionsForm" + labelCol={{ flex: "150px" }} + wrapperCol={{ flex: "auto" }} + labelWrap + initialValues={{ + name: `integration-${formatDate(new Date())}`, + language: "xml", + container: { + imagePoolPolicy: "IfNotPresent", + }, + }} + onFinish={(values) => { + setConfirmLoading(true); + try { + const result = onSubmit?.(values); + if (result instanceof Promise) { + result + .then(() => { + closeContainingModal(); + setConfirmLoading(false); + }) + .catch(() => { + setConfirmLoading(false); + }); + } else { + closeContainingModal(); + setConfirmLoading(false); + } + } catch (error) { + notificationService.errorWithDetails( + "Failed to submit form", + "An exception has been throws from the form submit callback", + error, + ); + setConfirmLoading(false); + } + }} + > + + + + + + + + + ({ value: domain.id, label: domain.name, }))} /> - + domain.name)} + loading={isDomainsLoading} + mode="tags" + allowClear + labelRender={renderOption} + optionRender={renderOption} + options={domains.map((domain) => ({ + value: domain.id, + label: domain.name + }))} + onChange={(values) => { + onChange?.(values.map(name =>({ name, type: getDomainType(name, domains )}))); + }} + > + ); +}; diff --git a/src/components/admin_tools/domains/DomainsTable.tsx b/src/components/admin_tools/domains/DomainsTable.tsx index 96b9f360..af9400da 100644 --- a/src/components/admin_tools/domains/DomainsTable.tsx +++ b/src/components/admin_tools/domains/DomainsTable.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Table, Button, Typography } from "antd"; +import { Table, Button, Typography, Space, Tag } from "antd"; import type { ColumnsType } from "antd/es/table"; import { EngineTable } from "./EngineTable"; import { useEngines } from "./hooks/useEngines"; import tableStyles from "./Tables.module.css"; -import { EngineDomain } from "../../../api/apiTypes.ts"; +import { DomainType, EngineDomain } from "../../../api/apiTypes.ts"; interface Props { domains: EngineDomain[]; @@ -38,6 +38,11 @@ const DomainsTable: React.FC = ({ domains, isLoading = false }) => { title: Domain, dataIndex: "name", key: "name", + render: (_: unknown, domain: EngineDomain) => { + return domain.type === DomainType.MICRO + ? {domain.name}micro + : domain.name; + } }, { title: Version, diff --git a/src/components/modal/DeployChains.tsx b/src/components/modal/DeployChains.tsx index d86cfa53..30d19293 100644 --- a/src/components/modal/DeployChains.tsx +++ b/src/components/modal/DeployChains.tsx @@ -1,14 +1,11 @@ -import React, { useCallback, useState } from "react"; +import React, { useState } from "react"; import { BulkDeploymentSnapshotAction, DomainType, - EngineDomain, } from "../../api/apiTypes.ts"; import { useModalContext } from "../../ModalContextProvider.tsx"; -import { Modal, Button, Form, Select, Space, Tag } from "antd"; -import { useDomains } from "../../hooks/useDomains.tsx"; -import { LabelInValueType } from "rc-select/lib/Select"; -import type { FlattenOptionData } from "rc-select/lib/interface"; +import { Modal, Button, Form, Select } from "antd"; +import { Domain, SelectDomains } from "../SelectDomains.tsx"; export type CamelKDeploy = { name: string; @@ -25,7 +22,7 @@ export type DeployRequest = { }; type DeployOptions = { - domains: string[]; + domains: Domain[]; snapshotAction: BulkDeploymentSnapshotAction; }; @@ -33,22 +30,12 @@ type DeployChainsProps = { onSubmit?: (options: DeployRequest) => void; }; -function getDomainType(domainId: string, domains: EngineDomain[]): DomainType { - return ( - domains.find((domain) => domainId === domain.id)?.type ?? DomainType.MICRO - ); -} - -function createDeployRequest( - deployOptions: DeployOptions, - domains: EngineDomain[], -): DeployRequest { +function createDeployRequest(deployOptions: DeployOptions): DeployRequest { const nativeDomains: string[] = []; const camelKDomains: string[] = []; for (const domain of deployOptions.domains) { - const domainType = getDomainType(domain, domains); - (domainType === DomainType.MICRO ? camelKDomains : nativeDomains).push( - domain, + (domain.type === DomainType.MICRO ? camelKDomains : nativeDomains).push( + domain.name, ); } return { @@ -66,22 +53,6 @@ function createDeployRequest( export const DeployChains: React.FC = ({ onSubmit }) => { const { closeContainingModal } = useModalContext(); const [confirmLoading, setConfirmLoading] = useState(false); - const { isLoading: isDomainsLoading, domains } = useDomains(); - - const renderOption = useCallback( - (props: LabelInValueType | FlattenOptionData) => { - const domainType = getDomainType(props.value?.toString() ?? "", domains); - return domainType === DomainType.MICRO ? ( - - micro - {props.value} - - ) : ( - props.label - ); - }, - [domains], - ); return ( = ({ onSubmit }) => { wrapperCol={{ flex: "auto" }} id="deployOptionsForm" initialValues={{ - domains: ["default"], + domains: [{ name: "default", type: DomainType.NATIVE }], snapshotAction: BulkDeploymentSnapshotAction.CREATE_NEW, }} onFinish={(values) => { setConfirmLoading(true); - const deployRequest = createDeployRequest(values, domains); + const deployRequest = createDeployRequest(values); try { onSubmit?.(deployRequest); closeContainingModal(); @@ -128,17 +99,7 @@ export const DeployChains: React.FC = ({ onSubmit }) => { label={"Engine domains"} rules={[{ required: true }]} > - void | Promise; + onSubmit?: (domains: Domain[]) => void | Promise; }; type SaveAndDeployFormData = { - domain: string; + domains: Domain[]; }; export const SaveAndDeploy: React.FC = ({ @@ -19,14 +20,6 @@ export const SaveAndDeploy: React.FC = ({ const [form] = Form.useForm(); const [confirmLoading, setConfirmLoading] = useState(false); const { closeContainingModal } = useModalContext(); - const { isLoading: domainsLoading, domains } = useDomains(); - - const domainOptions: SelectProps["options"] = domains - ?.sort((d1, d2) => d1.name.localeCompare(d2.name)) - .map((domain) => ({ - label: domain.name, - value: domain.id, - })); const handleSubmit = (data: SaveAndDeployFormData) => { if (!chainId) { @@ -34,7 +27,7 @@ export const SaveAndDeploy: React.FC = ({ } setConfirmLoading(true); try { - const result = onSubmit?.(data.domain); + const result = onSubmit?.(data.domains); if (result instanceof Promise) { void result.finally(() => { setConfirmLoading(false); @@ -75,18 +68,22 @@ export const SaveAndDeploy: React.FC = ({ id="saveAndDeployForm" form={form} layout="horizontal" + initialValues={{ + domains: [{ name: "default", type: DomainType.NATIVE }], + }} labelCol={{ span: 4 }} style={{ maxWidth: 600 }} - disabled={domainsLoading} labelWrap onFinish={(values) => handleSubmit(values)} > -
Date: Tue, 10 Feb 2026 10:49:12 +0300 Subject: [PATCH 14/19] feat: aligned types with runtime catalog --- src/api/apiTypes.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index dc74022a..8b20635f 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -1164,10 +1164,24 @@ export type CustomResourceBuildRequest = { export type CustomResourceOptions = { language?: string; name?: string; + namespace?: string; container?: ContainerOptions; + monitoring?: MonitoringOptions; + service?: ServiceOptions; environment?: Record; + resources?: string[]; + serviceAccount?: string; }; +export type MonitoringOptions = { + enabled: boolean; + interval: string; +} + +export type ServiceOptions = { + enabled: boolean; +} + export type ContainerOptions = { image?: string; imagePoolPolicy?: "Always" | "Never" | "IfNotPresent"; From 1999671b4f287dc585269598ab6d781d77a9e057 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:01:21 +0300 Subject: [PATCH 15/19] feat: handled microdomains in the deployment page --- src/api/apiTypes.ts | 1 + src/pages/Deployments.tsx | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index 8b20635f..ed75b502 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -265,6 +265,7 @@ export type Deployment = { snapshotId: string; name: string; domain: string; + domainType: DomainType; createdWhen: number; createdBy: User; runtime?: RuntimeStates; diff --git a/src/pages/Deployments.tsx b/src/pages/Deployments.tsx index ab9c45e0..adf0758a 100644 --- a/src/pages/Deployments.tsx +++ b/src/pages/Deployments.tsx @@ -1,12 +1,16 @@ import React from "react"; -import { FloatButton, Table, Tooltip } from "antd"; +import { FloatButton, Space, Table, Tag, Tooltip } from "antd"; import { useDeployments } from "../hooks/useDeployments.tsx"; import { useParams } from "react-router"; import { TableProps } from "antd/lib/table"; -import { CreateDeploymentRequest, Deployment } from "../api/apiTypes.ts"; +import { + CreateDeploymentRequest, + Deployment, + DomainType, +} from "../api/apiTypes.ts"; import { DeploymentRuntimeStates } from "../components/deployment_runtime_states/DeploymentRuntimeStates.tsx"; import { useSnapshots } from "../hooks/useSnapshots.tsx"; -import { formatTimestamp } from "../misc/format-utils.ts"; +import { formatOptional, formatTimestamp } from "../misc/format-utils.ts"; import { useModalsContext } from "../Modals.tsx"; import { DeploymentCreate } from "../components/modal/DeploymentCreate.tsx"; import { api } from "../api/api.ts"; @@ -34,7 +38,20 @@ export const Deployments: React.FC = () => { ), }, - { title: "Domain", dataIndex: "domain", key: "domain" }, + { + title: "Domain", + dataIndex: "domain", + key: "domain", + render: (_, deployment) => + deployment.domainType === DomainType.MICRO ? ( + + {deployment.domain} + micro + + ) : ( + deployment.domain + ), + }, { title: "Status", dataIndex: "runtime", @@ -51,7 +68,9 @@ export const Deployments: React.FC = () => { title: "Created By", dataIndex: "createdBy", key: "createdBy", - render: (_, deployment) => <>{deployment.createdBy.username}, + render: (_, deployment) => ( + <>{formatOptional(deployment.createdBy?.username)} + ), }, { title: "Created At", From 5c713a65c62ea53892806e983a4bbd1d1c642532 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:16:17 +0300 Subject: [PATCH 16/19] feat: implemented removing a chain from a microdomain --- src/api/api.ts | 2 ++ src/api/rest/restApi.ts | 6 ++++++ src/api/rest/vscodeExtensionApi.ts | 3 +++ src/pages/Deployments.tsx | 6 +++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/api/api.ts b/src/api/api.ts index f772b9da..41d48740 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -480,6 +480,8 @@ export interface Api { deployMicroDomain(request: MicroDomainDeployRequest): Promise; deleteMicroDomain(name: string): Promise; + + deleteChainFromMicroDomain(name: string, chainId: string): Promise; } export const api: Api = isVsCode ? new VSCodeExtensionApi() : new RestApi(); diff --git a/src/api/rest/restApi.ts b/src/api/rest/restApi.ts index 59e127a5..00e1991f 100644 --- a/src/api/rest/restApi.ts +++ b/src/api/rest/restApi.ts @@ -1562,4 +1562,10 @@ export class RestApi implements Api { `/api/v1/${getAppName()}/catalog/cr/${name}`, ); }; + + deleteChainFromMicroDomain = async (name: string, chainId: string): Promise => { + await this.instance.delete( + `/api/v1/${getAppName()}/catalog/cr/${name}/${chainId}`, + ); + } } diff --git a/src/api/rest/vscodeExtensionApi.ts b/src/api/rest/vscodeExtensionApi.ts index 35c7e789..30844080 100644 --- a/src/api/rest/vscodeExtensionApi.ts +++ b/src/api/rest/vscodeExtensionApi.ts @@ -963,6 +963,9 @@ export class VSCodeExtensionApi implements Api { deleteMicroDomain(): Promise { throw new Error("Method deleteMicroDomain not implemented."); } + deleteChainFromMicroDomain(): Promise { + throw new Error("Method deleteChainFromMicroDomain not implemented."); + } } interface VSCodeApi { diff --git a/src/pages/Deployments.tsx b/src/pages/Deployments.tsx index adf0758a..941ec021 100644 --- a/src/pages/Deployments.tsx +++ b/src/pages/Deployments.tsx @@ -102,7 +102,11 @@ export const Deployments: React.FC = () => { const deleteDeployment = async (deployment: Deployment) => { try { - await api.deleteDeployment(deployment.id); + if (deployment.domainType === DomainType.MICRO) { + await api.deleteChainFromMicroDomain(deployment.domain, deployment.chainId); + } else { + await api.deleteDeployment(deployment.id); + } removeDeployment(deployment); } catch (error) { notificationService.requestFailed("Failed to delete deployment", error); From d3f88ed29f4bb20970f5790a61de998a39891dd5 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:37:41 +0300 Subject: [PATCH 17/19] feat: updated create deployment dialog --- src/components/modal/DeploymentCreate.tsx | 59 ++++++++++++----------- src/pages/Deployments.tsx | 34 +++++++++---- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/components/modal/DeploymentCreate.tsx b/src/components/modal/DeploymentCreate.tsx index a9b11f0b..019deb3b 100644 --- a/src/components/modal/DeploymentCreate.tsx +++ b/src/components/modal/DeploymentCreate.tsx @@ -1,49 +1,45 @@ import { Button, Form, Modal, Select, SelectProps } from "antd"; import React, { useState } from "react"; import { useModalContext } from "../../ModalContextProvider.tsx"; -import { useDomains } from "../../hooks/useDomains.tsx"; import { useSnapshots } from "../../hooks/useSnapshots.tsx"; -import { CreateDeploymentRequest } from "../../api/apiTypes.ts"; +import { DomainType } from "../../api/apiTypes.ts"; +import { Domain, SelectDomains } from "../SelectDomains.tsx"; -type Props = { +export type CreateDeploymentOptions = { + domains: Domain[]; + snapshotId: string; +}; + +type CreateDeploymentProps = { chainId?: string; - onSubmit?: (request: CreateDeploymentRequest) => void | Promise; + onSubmit?: (request: CreateDeploymentOptions) => void | Promise; }; -export const DeploymentCreate: React.FC = ({ chainId, onSubmit }) => { +export const DeploymentCreate: React.FC = ({ + chainId, + onSubmit, +}) => { const [form] = Form.useForm(); const [confirmLoading, setConfirmLoading] = useState(false); const { closeContainingModal } = useModalContext(); - const { isLoading: domainsLoading, domains } = useDomains(); const { isLoading: snapshotsLoading, snapshots } = useSnapshots(chainId); - const domainOptions: SelectProps["options"] = domains - ?.sort((d1, d2) => d1.name.localeCompare(d2.name)) - .map((domain) => ({ - label: domain.name, - value: domain.id, - })); - const snapshotOptions: SelectProps["options"] = snapshots - ?.sort((s1, s2) => s2.modifiedWhen - s1.modifiedWhen) + ?.sort((s1, s2) => (s2.modifiedWhen ?? 0) - (s1.modifiedWhen ?? 0)) .map((snapshot) => ({ label: snapshot.name, value: snapshot.id, })) ?? []; - const handleOk = (domain: string, snapshotId: string) => { + const handleOk = (domains: Domain[], snapshotId: string) => { if (!chainId) { return; } setConfirmLoading(true); - const request: CreateDeploymentRequest = { - domain, - snapshotId, - suspended: false, - }; + const options: CreateDeploymentOptions = { domains, snapshotId }; try { - const result = onSubmit?.(request); + const result = onSubmit?.(options); if (result instanceof Promise) { void result.finally(() => { setConfirmLoading(false); @@ -79,27 +75,32 @@ export const DeploymentCreate: React.FC = ({ chainId, onSubmit }) => { form="deploymentCreateForm" htmlType={"submit"} loading={confirmLoading} - disabled={domainsLoading || snapshotsLoading} + disabled={snapshotsLoading} > Deploy , ]} > - + id="deploymentCreateForm" form={form} - onFinish={(values) => handleOk(values.domain, values.snapshot)} + initialValues={{ + domains: [{ name: "default", type: DomainType.NATIVE }], + }} + onFinish={(values) => handleOk(values.domains, values.snapshot)} layout="horizontal" labelCol={{ span: 4 }} style={{ maxWidth: 600 }} - disabled={domainsLoading || snapshotsLoading} + disabled={snapshotsLoading} > -
{ - if (sortOrder === "ascend") { - return a.name.localeCompare(b.name); - } else if (sortOrder === "descend") { - return b.name.localeCompare(a.name); - } - return 0; - }, - render: ( - value: string, - _record: NameValuePair, - index: number, - ) => { - return ( - - values={{ value }} - editor={} - viewer={value} - initialActive={value === ""} - onSubmit={({ value }) => { - if (tableData?.some((r) => r.name === value)) { - void messageApi.error(`Already exists: ${value}`); - } else { - updateRecord(index, { name: value }); - } - }} - /> - ); - }, - }, - { - key: "value", - title: "Value", - dataIndex: "value", - sorter: (a, b, sortOrder) => { - if (sortOrder === "ascend") { - return a.value.localeCompare(b.value); - } else if (sortOrder === "descend") { - return b.value.localeCompare(a.value); - } - return 0; - }, - render: ( - value: string, - _record: NameValuePair, - index: number, - ) => { - return ( - - values={{ value }} - editor={} - viewer={value} - onSubmit={({ value }) => { - updateRecord(index, { value }); - }} - /> - ); - }, - }, - { - key: "actions", - className: "actions-column", - width: 40, - align: "right", - render: (_, _record, index: number) => { - return ( - , - , - ]} - > - - id="crOptionsForm" - labelCol={{ flex: "150px" }} - wrapperCol={{ flex: "auto" }} - labelWrap - initialValues={{ - name: `integration-${formatDate(new Date()).replaceAll("_", "-").toLowerCase()}`, - language: "xml", - container: { - imagePoolPolicy: "IfNotPresent", - }, - }} - onFinish={(values) => { - setConfirmLoading(true); - try { - const result = onSubmit?.(values); - if (result instanceof Promise) { - result - .then(() => { - closeContainingModal(); - setConfirmLoading(false); - }) - .catch(() => { - setConfirmLoading(false); - }); - } else { - closeContainingModal(); - setConfirmLoading(false); - } - } catch (error) { - notificationService.errorWithDetails( - "Failed to submit form", - "An exception has been throws from the form submit callback", - error, - ); - setConfirmLoading(false); - } - }} - > - - - - - - - - -