|
@@ -0,0 +1,535 @@
|
|
|
+import { Create, useForm, useSelect } from "@refinedev/antd";
|
|
|
+import { Form, Input, Tabs, Card, Button, Select, InputNumber, Switch, Space, Divider } from "antd";
|
|
|
+import { useState } from "react";
|
|
|
+import { TemplateFormData, TemplateConfig, AppConfig, NetworkPolicy, SecretConfig } from "../../types/template";
|
|
|
+import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
|
|
+import { useApiUrl } from "@refinedev/core";
|
|
|
+
|
|
|
+const { TabPane } = Tabs;
|
|
|
+const { TextArea } = Input;
|
|
|
+
|
|
|
+export const TemplateCreate = () => {
|
|
|
+ const apiUrl = useApiUrl();
|
|
|
+ const { formProps, saveButtonProps } = useForm<TemplateFormData>({
|
|
|
+ redirect: "list",
|
|
|
+ onMutationSuccess: (data) => {
|
|
|
+ console.log("Template created successfully:", data);
|
|
|
+ },
|
|
|
+ meta: {
|
|
|
+ onSubmit: (values: { config: { apps: any[]; networkPolicies: any[]; }; }) => {
|
|
|
+ // Convert exposedPorts to integers for all apps
|
|
|
+ if (values.config.apps) {
|
|
|
+ values.config.apps = values.config.apps.map((app: { exposedPorts: any[]; }) => ({
|
|
|
+ ...app,
|
|
|
+ exposedPorts: app.exposedPorts?.map((port: string) =>
|
|
|
+ typeof port === 'string' ? parseInt(port, 10) : port
|
|
|
+ ),
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Convert network policy ports to integers as well
|
|
|
+ if (values.config.networkPolicies) {
|
|
|
+ values.config.networkPolicies = values.config.networkPolicies.map((policy: { ports: any[]; }) => ({
|
|
|
+ ...policy,
|
|
|
+ ports: policy.ports?.map((port: string) =>
|
|
|
+ typeof port === 'string' ? parseInt(port, 10) : port
|
|
|
+ ),
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ return values;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const [activeTabKey, setActiveTabKey] = useState("basic");
|
|
|
+
|
|
|
+ // For app selection in the template
|
|
|
+ const { selectProps: appSelectProps } = useSelect({
|
|
|
+ resource: "apps",
|
|
|
+ optionLabel: "name",
|
|
|
+ optionValue: "id",
|
|
|
+ });
|
|
|
+
|
|
|
+ // Initialize with default values
|
|
|
+ const initialValues: Partial<TemplateFormData> = {
|
|
|
+ version: "1.0.0",
|
|
|
+ config: {
|
|
|
+ apps: [],
|
|
|
+ networkPolicies: [],
|
|
|
+ envVariables: {},
|
|
|
+ secrets: []
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Create saveButtonProps={saveButtonProps}>
|
|
|
+ <Form
|
|
|
+ {...formProps}
|
|
|
+ layout="vertical"
|
|
|
+ initialValues={initialValues}
|
|
|
+ onValuesChange={(_, allValues) => {
|
|
|
+ console.log("Form values:", allValues);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Tabs
|
|
|
+ activeKey={activeTabKey}
|
|
|
+ onChange={setActiveTabKey}
|
|
|
+ >
|
|
|
+ <TabPane tab="Basic Information" key="basic">
|
|
|
+ <Card title="Template Details" bordered={false}>
|
|
|
+ <Form.Item
|
|
|
+ label="Template Name"
|
|
|
+ name="name"
|
|
|
+ rules={[{ required: true, message: 'Please enter a template name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="Enter template name" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Description"
|
|
|
+ name="description"
|
|
|
+ rules={[{ required: true, message: 'Please enter a description' }]}
|
|
|
+ >
|
|
|
+ <TextArea rows={3} placeholder="Describe this template" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Version"
|
|
|
+ name="version"
|
|
|
+ rules={[{ required: true, message: 'Please enter a version' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="1.0.0" />
|
|
|
+ </Form.Item>
|
|
|
+ </Card>
|
|
|
+ </TabPane>
|
|
|
+
|
|
|
+ <TabPane tab="Apps Configuration" key="apps">
|
|
|
+ <Card title="Configure Apps" bordered={false}>
|
|
|
+ <Form.List name={["config", "apps"]}>
|
|
|
+ {(fields, { add, remove }) => (
|
|
|
+ <>
|
|
|
+ {fields.map(({ key, name, ...restField }) => (
|
|
|
+ <Card
|
|
|
+ key={key}
|
|
|
+ title={`App ${name + 1}`}
|
|
|
+ style={{ marginBottom: 16 }}
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ onClick={() => remove(name)}
|
|
|
+ danger
|
|
|
+ icon={<MinusCircleOutlined />}
|
|
|
+ >
|
|
|
+ Remove
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "id"]}
|
|
|
+ label="App"
|
|
|
+ rules={[{ required: true, message: 'Please select an app' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ {...appSelectProps}
|
|
|
+ placeholder="Select an app"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "name"]}
|
|
|
+ label="Display Name"
|
|
|
+ rules={[{ required: true, message: 'Please enter a name for this app instance' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="Name for this app instance" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Exposed Ports"
|
|
|
+ name={[name, "exposedPorts"]}
|
|
|
+ getValueFromEvent={(values) => {
|
|
|
+ return values.map((value: string | number) =>
|
|
|
+ typeof value === 'string' ? parseInt(value, 10) : value
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="tags"
|
|
|
+ tokenSeparators={[',']}
|
|
|
+ placeholder="Enter ports (e.g., 8080, 443)"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Public Access"
|
|
|
+ name={[name, "publicAccess"]}
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={false}
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Service Mesh"
|
|
|
+ name={[name, "serviceMesh"]}
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={false}
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Divider orientation="left">Resources</Divider>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "resources", "cpu"]}
|
|
|
+ label="CPU"
|
|
|
+ rules={[{ required: true, message: 'Please specify CPU resources' }]}
|
|
|
+ initialValue="0.5"
|
|
|
+ >
|
|
|
+ <Input placeholder="0.5" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "resources", "memory"]}
|
|
|
+ label="Memory"
|
|
|
+ rules={[{ required: true, message: 'Please specify memory resources' }]}
|
|
|
+ initialValue="512Mi"
|
|
|
+ >
|
|
|
+ <Input placeholder="512Mi" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "resources", "storage"]}
|
|
|
+ label="Storage"
|
|
|
+ rules={[{ required: true, message: 'Please specify storage resources' }]}
|
|
|
+ initialValue="1Gi"
|
|
|
+ >
|
|
|
+ <Input placeholder="1Gi" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Divider orientation="left">Autoscaling</Divider>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Enable Autoscaling"
|
|
|
+ name={[name, "autoscaling", "enabled"]}
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={false}
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => {
|
|
|
+ return prevValues?.config?.apps?.[name]?.autoscaling?.enabled !==
|
|
|
+ currentValues?.config?.apps?.[name]?.autoscaling?.enabled;
|
|
|
+ }}>
|
|
|
+ {({ getFieldValue }) => {
|
|
|
+ const enabled = getFieldValue(['config', 'apps', name, 'autoscaling', 'enabled']);
|
|
|
+
|
|
|
+ if (!enabled) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "autoscaling", "minReplicas"]}
|
|
|
+ label="Min Replicas"
|
|
|
+ initialValue={1}
|
|
|
+ >
|
|
|
+ <InputNumber min={1} max={10} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "autoscaling", "maxReplicas"]}
|
|
|
+ label="Max Replicas"
|
|
|
+ initialValue={3}
|
|
|
+ >
|
|
|
+ <InputNumber min={1} max={20} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "autoscaling", "cpuThreshold"]}
|
|
|
+ label="CPU Threshold (%)"
|
|
|
+ initialValue={80}
|
|
|
+ >
|
|
|
+ <InputNumber min={1} max={100} />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "autoscaling", "metric"]}
|
|
|
+ label="Scaling Metric"
|
|
|
+ initialValue="cpu"
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ options={[
|
|
|
+ { label: "CPU", value: "cpu" },
|
|
|
+ { label: "Memory", value: "memory" },
|
|
|
+ { label: "Requests", value: "requests" }
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Divider orientation="left">Environment Overrides</Divider>
|
|
|
+ <Form.List name={[name, "envOverrides"]}>
|
|
|
+ {(envFields, { add: addEnv, remove: removeEnv }) => (
|
|
|
+ <>
|
|
|
+ {envFields.map(({ key, name: envName, ...restEnvField }) => (
|
|
|
+ <Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
|
|
+ <Form.Item
|
|
|
+ {...restEnvField}
|
|
|
+ name={[envName, "key"]}
|
|
|
+ rules={[{ required: true, message: 'Missing key' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="ENV_KEY" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ {...restEnvField}
|
|
|
+ name={[envName, "value"]}
|
|
|
+ rules={[{ required: true, message: 'Missing value' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="value" />
|
|
|
+ </Form.Item>
|
|
|
+ <MinusCircleOutlined onClick={() => removeEnv(envName)} />
|
|
|
+ </Space>
|
|
|
+ ))}
|
|
|
+ <Form.Item>
|
|
|
+ <Button type="dashed" onClick={() => addEnv()} block icon={<PlusOutlined />}>
|
|
|
+ Add Environment Variable
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form.List>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ <Form.Item>
|
|
|
+ <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
|
+ Add App
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form.List>
|
|
|
+ </Card>
|
|
|
+ </TabPane>
|
|
|
+
|
|
|
+ <TabPane tab="Network Policies" key="network">
|
|
|
+ <Card title="Network Policies" bordered={false}>
|
|
|
+ <Form.List name={["config", "networkPolicies"]}>
|
|
|
+ {(fields, { add, remove }) => (
|
|
|
+ <>
|
|
|
+ {fields.map(({ key, name, ...restField }) => (
|
|
|
+ <Card
|
|
|
+ key={key}
|
|
|
+ title={`Policy ${name + 1}`}
|
|
|
+ style={{ marginBottom: 16 }}
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ onClick={() => remove(name)}
|
|
|
+ danger
|
|
|
+ icon={<MinusCircleOutlined />}
|
|
|
+ >
|
|
|
+ Remove
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "name"]}
|
|
|
+ label="Policy Name"
|
|
|
+ rules={[{ required: true, message: 'Please enter a policy name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="Enter policy name" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "fromApps"]}
|
|
|
+ label="Source Apps"
|
|
|
+ rules={[{ required: true, message: 'Please select source apps' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="multiple"
|
|
|
+ placeholder="Select source apps"
|
|
|
+ >
|
|
|
+ {formProps.form?.getFieldValue(['config', 'apps'])?.map((app: AppConfig) => (
|
|
|
+ <Select.Option key={app.id} value={app.id}>
|
|
|
+ {app.name}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "toApps"]}
|
|
|
+ label="Destination Apps"
|
|
|
+ rules={[{ required: true, message: 'Please select destination apps' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="multiple"
|
|
|
+ placeholder="Select destination apps"
|
|
|
+ >
|
|
|
+ {formProps.form?.getFieldValue(['config', 'apps'])?.map((app: AppConfig) => (
|
|
|
+ <Select.Option key={app.id} value={app.id}>
|
|
|
+ {app.name}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Allowed Ports"
|
|
|
+ name={[name, "ports"]}
|
|
|
+ getValueFromEvent={(values) => {
|
|
|
+ return values.map((value: string | number) =>
|
|
|
+ typeof value === 'string' ? parseInt(value, 10) : value
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="tags"
|
|
|
+ tokenSeparators={[',']}
|
|
|
+ placeholder="Enter allowed ports (e.g., 8080, 443)"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Allow Egress"
|
|
|
+ name={[name, "allowEgress"]}
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={true}
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ <Form.Item>
|
|
|
+ <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
|
+ Add Network Policy
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form.List>
|
|
|
+ </Card>
|
|
|
+ </TabPane>
|
|
|
+
|
|
|
+ <TabPane tab="Environment & Secrets" key="env-secrets">
|
|
|
+ <Card title="Global Environment Variables" bordered={false}>
|
|
|
+ <Form.List name={["config", "envVariables"]}>
|
|
|
+ {(fields, { add, remove }) => (
|
|
|
+ <>
|
|
|
+ {fields.map(({ key, name, ...restField }) => (
|
|
|
+ <Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "key"]}
|
|
|
+ rules={[{ required: true, message: 'Missing key' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="ENV_KEY" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "value"]}
|
|
|
+ rules={[{ required: true, message: 'Missing value' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="value" />
|
|
|
+ </Form.Item>
|
|
|
+ <MinusCircleOutlined onClick={() => remove(name)} />
|
|
|
+ </Space>
|
|
|
+ ))}
|
|
|
+ <Form.Item>
|
|
|
+ <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
|
+ Add Environment Variable
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form.List>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card title="Secrets Configuration" bordered={false} style={{ marginTop: 16 }}>
|
|
|
+ <Form.List name={["config", "secrets"]}>
|
|
|
+ {(fields, { add, remove }) => (
|
|
|
+ <>
|
|
|
+ {fields.map(({ key, name, ...restField }) => (
|
|
|
+ <Card
|
|
|
+ key={key}
|
|
|
+ title={`Secret ${name + 1}`}
|
|
|
+ style={{ marginBottom: 16 }}
|
|
|
+ size="small"
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ onClick={() => remove(name)}
|
|
|
+ danger
|
|
|
+ icon={<MinusCircleOutlined />}
|
|
|
+ >
|
|
|
+ Remove
|
|
|
+ </Button>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "name"]}
|
|
|
+ label="Secret Name"
|
|
|
+ rules={[{ required: true, message: 'Please enter a secret name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="Enter secret name" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ name={[name, "description"]}
|
|
|
+ label="Description"
|
|
|
+ >
|
|
|
+ <Input placeholder="Describe this secret" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ {...restField}
|
|
|
+ label="Required"
|
|
|
+ name={[name, "required"]}
|
|
|
+ valuePropName="checked"
|
|
|
+ initialValue={true}
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+ </Card>
|
|
|
+ ))}
|
|
|
+ <Form.Item>
|
|
|
+ <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
|
|
+ Add Secret
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Form.List>
|
|
|
+ </Card>
|
|
|
+ </TabPane>
|
|
|
+ </Tabs>
|
|
|
+ </Form>
|
|
|
+ </Create>
|
|
|
+ );
|
|
|
+};
|