|
@@ -0,0 +1,747 @@
|
|
|
|
+import React, { useState } from 'react';
|
|
|
|
+import { Create, useForm, useSelect } from "@refinedev/antd";
|
|
|
|
+import { Form, Input, Button, Card, Table, Modal, Select, InputNumber, Switch, Tabs } from 'antd';
|
|
|
|
+import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
|
|
+import { AppFormData, ComponentConfig, NetworkPolicy, SecretConfig } from '../../types/app';
|
|
|
|
+
|
|
|
|
+const { TabPane } = Tabs;
|
|
|
|
+
|
|
|
|
+export const AppCreate = () => {
|
|
|
|
+ const { formProps, saveButtonProps, formLoading } = useForm<AppFormData>({});
|
|
|
|
+
|
|
|
|
+ // Add the component selection hook from the API
|
|
|
|
+ const { selectProps: componentSelectProps, queryResult: componentsQueryResult } = useSelect({
|
|
|
|
+ resource: "components",
|
|
|
|
+ optionLabel: "name",
|
|
|
|
+ optionValue: "id",
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // Helper function to get component name from ID
|
|
|
|
+ const getComponentNameById = (id: string) => {
|
|
|
|
+ const component = componentsQueryResult.data?.data.find((item: any) => item.id === id);
|
|
|
|
+ return component ? component.name : id;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ // Initialize with default values
|
|
|
|
+ const initialValues: Partial<AppFormData> = {
|
|
|
|
+ version: "1.0.0",
|
|
|
|
+ config: {
|
|
|
|
+ components: [],
|
|
|
|
+ networkPolicies: [],
|
|
|
|
+ envVariables: {},
|
|
|
|
+ secrets: []
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const [componentModalVisible, setComponentModalVisible] = useState(false);
|
|
|
|
+ const [policyModalVisible, setPolicyModalVisible] = useState(false);
|
|
|
|
+ const [secretModalVisible, setSecretModalVisible] = useState(false);
|
|
|
|
+
|
|
|
|
+ // Current form values through formProps.getFieldsValue()
|
|
|
|
+ const getFormValues = (): Partial<AppFormData> => {
|
|
|
|
+ if (!formProps.form) {
|
|
|
|
+ return {};
|
|
|
|
+ }
|
|
|
|
+ return formProps.form.getFieldsValue() || {};
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // Components management
|
|
|
|
+ const [currentComponent, setCurrentComponent] = useState<Partial<ComponentConfig>>({
|
|
|
|
+ resources: { cpu: '0.5', memory: '512Mi', storage: '1Gi' },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const openComponentModal = () => {
|
|
|
|
+ setCurrentComponent({
|
|
|
|
+ resources: { cpu: '0.5', memory: '512Mi', storage: '1Gi' },
|
|
|
|
+ publicAccess: false,
|
|
|
|
+ serviceMesh: true,
|
|
|
|
+ });
|
|
|
|
+ setComponentModalVisible(true);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const addComponent = () => {
|
|
|
|
+ if (!currentComponent.id) {
|
|
|
|
+ // Require a component to be selected
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Component Required',
|
|
|
|
+ content: 'Please select a component from the dropdown'
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const components = [...(values?.config?.components || [])];
|
|
|
|
+
|
|
|
|
+ // Check if component already exists in the blueprint
|
|
|
|
+ const exists = components.some(c => c.id === currentComponent.id);
|
|
|
|
+ if (exists) {
|
|
|
|
+ // Show a message that the component already exists
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Component Already Exists',
|
|
|
|
+ content: `The component "${currentComponent.name || currentComponent.id}" has already been added to this blueprint.`
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Find complete component data from query result
|
|
|
|
+ const componentData = componentsQueryResult.data?.data?.find(
|
|
|
|
+ (item: any) => item.id === currentComponent.id
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ components.push({
|
|
|
|
+ id: currentComponent.id,
|
|
|
|
+ name: componentData?.name || currentComponent.name || "",
|
|
|
|
+ resources: currentComponent.resources || { cpu: '0.5', memory: '512Mi', storage: '1Gi' },
|
|
|
|
+ publicAccess: currentComponent.publicAccess === undefined ? false : currentComponent.publicAccess,
|
|
|
|
+ serviceMesh: currentComponent.serviceMesh === undefined ? true : currentComponent.serviceMesh,
|
|
|
|
+ } as ComponentConfig);
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ components
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ setComponentModalVisible(false);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const removeComponent = (id: string) => {
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const components = values.config?.components?.filter((c: { id: string; }) => c.id !== id) || [];
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ components
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // Network Policy management
|
|
|
|
+ const [currentPolicy, setCurrentPolicy] = useState<Partial<NetworkPolicy>>({});
|
|
|
|
+
|
|
|
|
+ // TypeScript helper for policy action
|
|
|
|
+ const setPolicyAction = (action: 'allow' | 'deny') => {
|
|
|
|
+ setCurrentPolicy({ ...currentPolicy, action });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const openPolicyModal = () => {
|
|
|
|
+ setCurrentPolicy({});
|
|
|
|
+ setPolicyModalVisible(true);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const addPolicy = () => {
|
|
|
|
+ // Validate required fields
|
|
|
|
+ if (!currentPolicy.name) {
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Policy Name Required',
|
|
|
|
+ content: 'Please enter a name for this network policy'
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!currentPolicy.sourceComponents || currentPolicy.sourceComponents.length === 0) {
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Source Components Required',
|
|
|
|
+ content: 'Please select at least one source component'
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!currentPolicy.targetComponents || currentPolicy.targetComponents.length === 0) {
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Target Components Required',
|
|
|
|
+ content: 'Please select at least one target component'
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const policies = [...(values.config?.networkPolicies || [])];
|
|
|
|
+
|
|
|
|
+ // Check if policy with same name already exists
|
|
|
|
+ const exists = policies.some(p => p.name === currentPolicy.name);
|
|
|
|
+ if (exists) {
|
|
|
|
+ Modal.error({
|
|
|
|
+ title: 'Policy Already Exists',
|
|
|
|
+ content: `A network policy with the name "${currentPolicy.name}" already exists.`
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ policies.push({
|
|
|
|
+ ...currentPolicy,
|
|
|
|
+ name: currentPolicy.name,
|
|
|
|
+ sourceComponents: currentPolicy.sourceComponents || [],
|
|
|
|
+ targetComponents: currentPolicy.targetComponents || [],
|
|
|
|
+ ports: currentPolicy.ports || [],
|
|
|
|
+ protocols: currentPolicy.protocols || ['TCP'],
|
|
|
|
+ priority: currentPolicy.priority || 100,
|
|
|
|
+ action: currentPolicy.action || 'allow',
|
|
|
|
+ } as NetworkPolicy);
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ networkPolicies: policies
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ setPolicyModalVisible(false);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const removePolicy = (name: string) => {
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const policies = values.config?.networkPolicies?.filter((p: { name: string; }) => p.name !== name) || [];
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ networkPolicies: policies
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // Secret management
|
|
|
|
+ const [currentSecret, setCurrentSecret] = useState<Partial<SecretConfig>>({});
|
|
|
|
+
|
|
|
|
+ // TypeScript helper for secret type
|
|
|
|
+ const setSecretType = (type: 'env' | 'file' | 'key') => {
|
|
|
|
+ setCurrentSecret({ ...currentSecret, type });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const openSecretModal = () => {
|
|
|
|
+ setCurrentSecret({});
|
|
|
|
+ setSecretModalVisible(true);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const addSecret = () => {
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const secrets = [...(values.config?.secrets || [])];
|
|
|
|
+
|
|
|
|
+ secrets.push({
|
|
|
|
+ ...currentSecret,
|
|
|
|
+ name: currentSecret.name || `secret-${Date.now()}`,
|
|
|
|
+ type: currentSecret.type || 'env',
|
|
|
|
+ } as SecretConfig);
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ secrets
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ setSecretModalVisible(false);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const removeSecret = (name: string) => {
|
|
|
|
+ const values = getFormValues();
|
|
|
|
+ const secrets = values.config?.secrets?.filter((s: { name: string; }) => s.name !== name) || [];
|
|
|
|
+
|
|
|
|
+ formProps.form?.setFieldsValue({
|
|
|
|
+ ...values,
|
|
|
|
+ config: {
|
|
|
|
+ ...values.config,
|
|
|
|
+ secrets
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <Create saveButtonProps={saveButtonProps}>
|
|
|
|
+ <Form {...formProps} layout="vertical" initialValues={initialValues}>
|
|
|
|
+ <Card title="Blueprint Information" style={{ marginBottom: 20 }}>
|
|
|
|
+ <Form.Item
|
|
|
|
+ label="Name"
|
|
|
|
+ name="name"
|
|
|
|
+ rules={[{ required: true, message: 'Please enter a blueprint name' }]}
|
|
|
|
+ >
|
|
|
|
+ <Input placeholder="Enter blueprint name" />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item
|
|
|
|
+ label="Description"
|
|
|
|
+ name="description"
|
|
|
|
+ rules={[{ required: true, message: 'Please enter a description' }]}
|
|
|
|
+ >
|
|
|
|
+ <Input.TextArea
|
|
|
|
+ rows={4}
|
|
|
|
+ placeholder="Describe the purpose of this blueprint"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item
|
|
|
|
+ label="Version"
|
|
|
|
+ name="version"
|
|
|
|
+ rules={[{ required: true, message: 'Please enter the version' }]}
|
|
|
|
+ >
|
|
|
|
+ <Input placeholder="E.g., 1.0.0" />
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Card>
|
|
|
|
+
|
|
|
|
+ <Tabs defaultActiveKey="components">
|
|
|
|
+ <TabPane tab="Components" key="components">
|
|
|
|
+ <Card
|
|
|
|
+ title="Components"
|
|
|
|
+ style={{ marginBottom: 20 }}
|
|
|
|
+ extra={
|
|
|
|
+ <Button
|
|
|
|
+ type="primary"
|
|
|
|
+ icon={<PlusOutlined />}
|
|
|
|
+ onClick={openComponentModal}
|
|
|
|
+ >
|
|
|
|
+ Add Component
|
|
|
|
+ </Button>
|
|
|
|
+ }
|
|
|
|
+ >
|
|
|
|
+ <Form.Item name={["config", "components"]} noStyle>
|
|
|
|
+ <Table
|
|
|
|
+ dataSource={getFormValues()?.config?.components || []}
|
|
|
|
+ rowKey="id"
|
|
|
|
+ pagination={false}
|
|
|
|
+ >
|
|
|
|
+ <Table.Column title="ID" dataIndex="id" />
|
|
|
|
+ <Table.Column title="Name" dataIndex="name" />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Public"
|
|
|
|
+ dataIndex="publicAccess"
|
|
|
|
+ render={(value) => value ? "Yes" : "No"}
|
|
|
|
+ />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Resources"
|
|
|
|
+ render={(_, record: ComponentConfig) => (
|
|
|
|
+ <>
|
|
|
|
+ <div>CPU: {record.resources.cpu}</div>
|
|
|
|
+ <div>Memory: {record.resources.memory}</div>
|
|
|
|
+ </>
|
|
|
|
+ )}
|
|
|
|
+ />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Actions"
|
|
|
|
+ render={(_, record: ComponentConfig) => (
|
|
|
|
+ <Button
|
|
|
|
+ danger
|
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
|
+ onClick={() => removeComponent(record.id)}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ />
|
|
|
|
+ </Table>
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Card>
|
|
|
|
+ </TabPane>
|
|
|
|
+
|
|
|
|
+ <TabPane tab="Network Policies" key="policies">
|
|
|
|
+ <Card
|
|
|
|
+ title="Network Policies"
|
|
|
|
+ style={{ marginBottom: 20 }}
|
|
|
|
+ extra={
|
|
|
|
+ <Button
|
|
|
|
+ type="primary"
|
|
|
|
+ icon={<PlusOutlined />}
|
|
|
|
+ onClick={openPolicyModal}
|
|
|
|
+ >
|
|
|
|
+ Add Policy
|
|
|
|
+ </Button>
|
|
|
|
+ }
|
|
|
|
+ >
|
|
|
|
+ <Form.Item name={["config", "networkPolicies"]} noStyle>
|
|
|
|
+ <Table
|
|
|
|
+ dataSource={getFormValues()?.config?.networkPolicies || []}
|
|
|
|
+ rowKey="name"
|
|
|
|
+ pagination={false}
|
|
|
|
+ >
|
|
|
|
+ <Table.Column title="Name" dataIndex="name" />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Action"
|
|
|
|
+ dataIndex="action"
|
|
|
|
+ render={(value) => value === 'allow' ? "Allow" : "Deny"}
|
|
|
|
+ />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Sources"
|
|
|
|
+ dataIndex="sourceComponents"
|
|
|
|
+ render={(componentIds: string[] | undefined) => {
|
|
|
|
+ if (!componentIds || componentIds.length === 0) {
|
|
|
|
+ return '-';
|
|
|
|
+ }
|
|
|
|
+ // Look up component names from IDs
|
|
|
|
+ const componentOptions = componentSelectProps.options || [];
|
|
|
|
+ const componentNames = componentIds.map((id: string) => {
|
|
|
|
+ const component = componentOptions.find((option: any) => option.value === id);
|
|
|
|
+ return component ? component.label : id;
|
|
|
|
+ });
|
|
|
|
+ return componentNames.join(', ');
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Targets"
|
|
|
|
+ dataIndex="targetComponents"
|
|
|
|
+ render={(componentIds: string[] | undefined) => {
|
|
|
|
+ if (!componentIds || componentIds.length === 0) {
|
|
|
|
+ return '-';
|
|
|
|
+ }
|
|
|
|
+ // Look up component names from IDs
|
|
|
|
+ const componentOptions = componentSelectProps.options || [];
|
|
|
|
+ const componentNames = componentIds.map((id: string) => {
|
|
|
|
+ const component = componentOptions.find((option: any) => option.value === id);
|
|
|
|
+ return component ? component.label : id;
|
|
|
|
+ });
|
|
|
|
+ return componentNames.join(', ');
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Actions"
|
|
|
|
+ render={(_, record: NetworkPolicy) => (
|
|
|
|
+ <Button
|
|
|
|
+ danger
|
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
|
+ onClick={() => removePolicy(record.name)}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ />
|
|
|
|
+ </Table>
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Card>
|
|
|
|
+ </TabPane>
|
|
|
|
+
|
|
|
|
+ <TabPane tab="Environment & Secrets" key="env">
|
|
|
|
+ <Card
|
|
|
|
+ title="Environment Variables"
|
|
|
|
+ style={{ marginBottom: 20 }}
|
|
|
|
+ >
|
|
|
|
+ <Form.Item
|
|
|
|
+ label="Environment Variables (KEY=value format, one per line)"
|
|
|
|
+ name={["config", "envVariables"]}
|
|
|
|
+ >
|
|
|
|
+ <Input.TextArea rows={6} />
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Card>
|
|
|
|
+
|
|
|
|
+ <Card
|
|
|
|
+ title="Secrets"
|
|
|
|
+ extra={
|
|
|
|
+ <Button
|
|
|
|
+ type="primary"
|
|
|
|
+ icon={<PlusOutlined />}
|
|
|
|
+ onClick={openSecretModal}
|
|
|
|
+ >
|
|
|
|
+ Add Secret
|
|
|
|
+ </Button>
|
|
|
|
+ }
|
|
|
|
+ >
|
|
|
|
+ <Form.Item name={["config", "secrets"]} noStyle>
|
|
|
|
+ <Table
|
|
|
|
+ dataSource={getFormValues()?.config?.secrets || []}
|
|
|
|
+ rowKey="name"
|
|
|
|
+ pagination={false}
|
|
|
|
+ >
|
|
|
|
+ <Table.Column title="Name" dataIndex="name" />
|
|
|
|
+ <Table.Column title="Type" dataIndex="type" />
|
|
|
|
+ <Table.Column title="Mount Path" dataIndex="mountPath" />
|
|
|
|
+ <Table.Column
|
|
|
|
+ title="Actions"
|
|
|
|
+ render={(_, record: SecretConfig) => (
|
|
|
|
+ <Button
|
|
|
|
+ danger
|
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
|
+ onClick={() => removeSecret(record.name)}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ />
|
|
|
|
+ </Table>
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Card>
|
|
|
|
+ </TabPane>
|
|
|
|
+ </Tabs>
|
|
|
|
+ </Form>
|
|
|
|
+
|
|
|
|
+ {/* Component Modal */}
|
|
|
|
+ <Modal
|
|
|
|
+ title="Add Component"
|
|
|
|
+ open={componentModalVisible}
|
|
|
|
+ onOk={addComponent}
|
|
|
|
+ onCancel={() => setComponentModalVisible(false)}
|
|
|
|
+ >
|
|
|
|
+ <Form layout="vertical">
|
|
|
|
+ <Form.Item label="Select Component" required>
|
|
|
|
+ <Select<string>
|
|
|
|
+ placeholder="Select a component"
|
|
|
|
+ showSearch
|
|
|
|
+ filterOption={(input, option) => {
|
|
|
|
+ if (!option?.label) return false;
|
|
|
|
+ return option.label.toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
|
|
|
+ }}
|
|
|
|
+ value={currentComponent.id}
|
|
|
|
+ onChange={(value) => {
|
|
|
|
+ if (!value) return;
|
|
|
|
+
|
|
|
|
+ // When selecting a component, find the component details
|
|
|
|
+ const selectedComponent = componentsQueryResult.data?.data?.find(
|
|
|
|
+ (item: any) => item.id === value
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ // Update state with the selected component
|
|
|
|
+ if (selectedComponent) {
|
|
|
|
+ setCurrentComponent({
|
|
|
|
+ ...currentComponent,
|
|
|
|
+ id: selectedComponent.id,
|
|
|
|
+ name: selectedComponent.name,
|
|
|
|
+ // You might want to prefill other properties from the component data if needed
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }}
|
|
|
|
+ options={componentSelectProps.options || []}
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
+ />
|
|
|
|
+ {currentComponent.name && (
|
|
|
|
+ <div style={{ marginTop: 8 }}>
|
|
|
|
+ Selected: <strong>{currentComponent.name}</strong>
|
|
|
|
+ </div>
|
|
|
|
+ )}
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Public Access">
|
|
|
|
+ <Switch
|
|
|
|
+ checked={currentComponent.publicAccess}
|
|
|
|
+ onChange={checked => setCurrentComponent({ ...currentComponent, publicAccess: checked })}
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Service Mesh">
|
|
|
|
+ <Switch
|
|
|
|
+ checked={currentComponent.serviceMesh}
|
|
|
|
+ onChange={checked => setCurrentComponent({ ...currentComponent, serviceMesh: checked })}
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="CPU Resources">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentComponent.resources?.cpu}
|
|
|
|
+ onChange={e => setCurrentComponent({
|
|
|
|
+ ...currentComponent,
|
|
|
|
+ resources: {
|
|
|
|
+ cpu: e.target.value,
|
|
|
|
+ memory: currentComponent.resources?.memory || '512Mi',
|
|
|
|
+ storage: currentComponent.resources?.storage || '1Gi'
|
|
|
|
+ }
|
|
|
|
+ })}
|
|
|
|
+ placeholder="e.g., 0.5, 1, 2"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Memory Resources">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentComponent.resources?.memory}
|
|
|
|
+ onChange={e => setCurrentComponent({
|
|
|
|
+ ...currentComponent,
|
|
|
|
+ resources: {
|
|
|
|
+ cpu: currentComponent.resources?.cpu || '0.5',
|
|
|
|
+ memory: e.target.value,
|
|
|
|
+ storage: currentComponent.resources?.storage || '1Gi'
|
|
|
|
+ }
|
|
|
|
+ })}
|
|
|
|
+ placeholder="e.g., 512Mi, 1Gi"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Storage Resources">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentComponent.resources?.storage}
|
|
|
|
+ onChange={e => setCurrentComponent({
|
|
|
|
+ ...currentComponent,
|
|
|
|
+ resources: {
|
|
|
|
+ cpu: currentComponent.resources?.cpu || '0.5',
|
|
|
|
+ memory: currentComponent.resources?.memory || '512Mi',
|
|
|
|
+ storage: e.target.value
|
|
|
|
+ }
|
|
|
|
+ })}
|
|
|
|
+ placeholder="e.g., 1Gi, 10Gi"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Form>
|
|
|
|
+ </Modal>
|
|
|
|
+
|
|
|
|
+ {/* Network Policy Modal */}
|
|
|
|
+ <Modal
|
|
|
|
+ title="Add Network Policy"
|
|
|
|
+ open={policyModalVisible}
|
|
|
|
+ onOk={addPolicy}
|
|
|
|
+ onCancel={() => setPolicyModalVisible(false)}
|
|
|
|
+ >
|
|
|
|
+ <Form layout="vertical">
|
|
|
|
+ <Form.Item label="Policy Name" required>
|
|
|
|
+ <Input
|
|
|
|
+ value={currentPolicy.name}
|
|
|
|
+ onChange={e => setCurrentPolicy({ ...currentPolicy, name: e.target.value })}
|
|
|
|
+ placeholder="Policy name"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Source Components" required>
|
|
|
|
+ <Select<string[]>
|
|
|
|
+ mode="multiple"
|
|
|
|
+ placeholder="Select source components"
|
|
|
|
+ value={currentPolicy.sourceComponents || []}
|
|
|
|
+ onChange={(values) => {
|
|
|
|
+ if (!values) return;
|
|
|
|
+ setCurrentPolicy({
|
|
|
|
+ ...currentPolicy,
|
|
|
|
+ sourceComponents: values
|
|
|
|
+ });
|
|
|
|
+ }}
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
+ options={componentSelectProps.options || []}
|
|
|
|
+ optionFilterProp="label"
|
|
|
|
+ showSearch
|
|
|
|
+ filterOption={(input, option) => {
|
|
|
|
+ if (!option?.label) return false;
|
|
|
|
+ return option.label.toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ {currentPolicy.sourceComponents && currentPolicy.sourceComponents.length > 0 && (
|
|
|
|
+ <div style={{ marginTop: 8 }}>
|
|
|
|
+ Selected: <strong>{currentPolicy.sourceComponents.length}</strong> component(s)
|
|
|
|
+ </div>
|
|
|
|
+ )}
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Target Components" required>
|
|
|
|
+ <Select<string[]>
|
|
|
|
+ mode="multiple"
|
|
|
|
+ placeholder="Select target components"
|
|
|
|
+ value={currentPolicy.targetComponents || []}
|
|
|
|
+ onChange={(values) => {
|
|
|
|
+ if (!values) return;
|
|
|
|
+ setCurrentPolicy({
|
|
|
|
+ ...currentPolicy,
|
|
|
|
+ targetComponents: values
|
|
|
|
+ });
|
|
|
|
+ }}
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
+ options={componentSelectProps.options || []}
|
|
|
|
+ optionFilterProp="label"
|
|
|
|
+ showSearch
|
|
|
|
+ filterOption={(input, option) => {
|
|
|
|
+ if (!option?.label) return false;
|
|
|
|
+ return option.label.toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ {currentPolicy.targetComponents && currentPolicy.targetComponents.length > 0 && (
|
|
|
|
+ <div style={{ marginTop: 8 }}>
|
|
|
|
+ Selected: <strong>{currentPolicy.targetComponents.length}</strong> component(s)
|
|
|
|
+ </div>
|
|
|
|
+ )}
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Ports (comma separated)">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentPolicy.ports?.join(',')}
|
|
|
|
+ onChange={e => {
|
|
|
|
+ const portInput = e.target.value;
|
|
|
|
+ // Handle empty input
|
|
|
|
+ if (!portInput.trim()) {
|
|
|
|
+ setCurrentPolicy({
|
|
|
|
+ ...currentPolicy,
|
|
|
|
+ ports: []
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Parse ports, filtering out any non-numeric values
|
|
|
|
+ const ports = portInput.split(',')
|
|
|
|
+ .map(item => item.trim())
|
|
|
|
+ .filter(item => /^\d+$/.test(item)) // Only keep numeric values
|
|
|
|
+ .map(item => parseInt(item, 10))
|
|
|
|
+ .filter(port => port > 0 && port <= 65535); // Only valid port numbers
|
|
|
|
+
|
|
|
|
+ setCurrentPolicy({
|
|
|
|
+ ...currentPolicy,
|
|
|
|
+ ports
|
|
|
|
+ });
|
|
|
|
+ }}
|
|
|
|
+ placeholder="e.g., 80,443,8080"
|
|
|
|
+ />
|
|
|
|
+ <div style={{ fontSize: '12px', marginTop: '4px', color: '#888' }}>
|
|
|
|
+ Valid port numbers: 1-65535
|
|
|
|
+ </div>
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Action">
|
|
|
|
+ <Select
|
|
|
|
+ value={currentPolicy.action}
|
|
|
|
+ onChange={(value) => setPolicyAction(value as 'allow' | 'deny')}
|
|
|
|
+ defaultValue="allow"
|
|
|
|
+ >
|
|
|
|
+ <Select.Option value="allow">Allow</Select.Option>
|
|
|
|
+ <Select.Option value="deny">Deny</Select.Option>
|
|
|
|
+ </Select>
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Priority">
|
|
|
|
+ <InputNumber
|
|
|
|
+ value={currentPolicy.priority}
|
|
|
|
+ onChange={value => setCurrentPolicy({ ...currentPolicy, priority: value as number })}
|
|
|
|
+ min={1}
|
|
|
|
+ defaultValue={100}
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Form>
|
|
|
|
+ </Modal>
|
|
|
|
+
|
|
|
|
+ {/* Secret Modal */}
|
|
|
|
+ <Modal
|
|
|
|
+ title="Add Secret"
|
|
|
|
+ open={secretModalVisible}
|
|
|
|
+ onOk={addSecret}
|
|
|
|
+ onCancel={() => setSecretModalVisible(false)}
|
|
|
|
+ >
|
|
|
|
+ <Form layout="vertical">
|
|
|
|
+ <Form.Item label="Secret Name" required>
|
|
|
|
+ <Input
|
|
|
|
+ value={currentSecret.name}
|
|
|
|
+ onChange={e => setCurrentSecret({ ...currentSecret, name: e.target.value })}
|
|
|
|
+ placeholder="Secret name"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Secret Type">
|
|
|
|
+ <Select
|
|
|
|
+ value={currentSecret.type}
|
|
|
|
+ onChange={(value) => setSecretType(value as 'env' | 'file' | 'key')}
|
|
|
|
+ defaultValue="env"
|
|
|
|
+ >
|
|
|
|
+ <Select.Option value="env">Environment Variable</Select.Option>
|
|
|
|
+ <Select.Option value="file">File</Select.Option>
|
|
|
|
+ <Select.Option value="key">Key</Select.Option>
|
|
|
|
+ </Select>
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Mount Path (for file type)">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentSecret.mountPath}
|
|
|
|
+ onChange={e => setCurrentSecret({ ...currentSecret, mountPath: e.target.value })}
|
|
|
|
+ placeholder="/path/to/mount"
|
|
|
|
+ disabled={currentSecret.type !== 'file'}
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+
|
|
|
|
+ <Form.Item label="Source">
|
|
|
|
+ <Input
|
|
|
|
+ value={currentSecret.source}
|
|
|
|
+ onChange={e => setCurrentSecret({ ...currentSecret, source: e.target.value })}
|
|
|
|
+ placeholder="e.g., env, vault://path"
|
|
|
|
+ />
|
|
|
|
+ </Form.Item>
|
|
|
|
+ </Form>
|
|
|
|
+ </Modal>
|
|
|
|
+ </Create>
|
|
|
|
+ );
|
|
|
|
+};
|