diff --git a/services/frontend/src/components/modals/kubernetes/service/Accordion.tsx b/services/frontend/src/components/modals/kubernetes/service/Accordion.tsx new file mode 100644 index 0000000..62eb2b6 --- /dev/null +++ b/services/frontend/src/components/modals/kubernetes/service/Accordion.tsx @@ -0,0 +1,70 @@ +import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/outline"; +import { styled } from "@mui/joy"; +import IconButton from "@mui/joy/IconButton"; +import { FunctionComponent, ReactElement, ReactNode } from "react"; +import { useAccordionState } from "../../../../hooks"; + +export interface IAccordionProps { + id: string; + title: string; + defaultOpen?: boolean; + children: ReactNode; +} + +const Root = styled("div")` + display: flex; + flex-direction: column; +`; + +const Top = styled("div")` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + &:hover { + cursor: pointer; + user-select: none; + } +`; + +const Title = styled("h5")` + font-size: 0.85rem; + color: #374151; + font-weight: 700; + width: 100%; + text-align: left; +`; + +const ExpandButton = styled(IconButton)` + border-radius: ${({ theme }) => theme.spacing(2)}; +`; + +const Bottom = styled("div")` + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(1)}; +`; + +const Accordion: FunctionComponent = ( + props: IAccordionProps +): ReactElement => { + const { id, defaultOpen = false, children, title } = props; + + const { open, toggle } = useAccordionState(id, defaultOpen); + + return ( + + + {title} + + {open && } + {!open && } + + + {open && {children}} + + ); +}; + +export default Accordion; diff --git a/services/frontend/src/components/modals/kubernetes/service/Create.tsx b/services/frontend/src/components/modals/kubernetes/service/Create.tsx new file mode 100644 index 0000000..359128f --- /dev/null +++ b/services/frontend/src/components/modals/kubernetes/service/Create.tsx @@ -0,0 +1,46 @@ +import { useCallback } from "react"; +import { CallbackFunction, IServiceNodeItem } from "../../../../types"; +import { IEditServiceForm } from "../../../../types/docker-compose"; +import { + getFinalValues, + getInitialValues, + tabs, + validationSchema +} from "./form-utils"; +import { toaster } from "../../../../utils"; +import FormModal from "../../../FormModal"; + +interface IModalServiceProps { + onHide: CallbackFunction; + onAddEndpoint: CallbackFunction; +} + +const CreateServiceModal = (props: IModalServiceProps) => { + const { onHide, onAddEndpoint } = props; + + const handleCreate = useCallback( + (finalValues: IServiceNodeItem, values: IEditServiceForm) => { + onHide(); + onAddEndpoint(finalValues); + toaster( + `Created "${values.serviceName}" service successfully`, + "success" + ); + }, + [onAddEndpoint, onHide] + ); + + return ( + + ); +}; + +export default CreateServiceModal; diff --git a/services/frontend/src/components/modals/kubernetes/service/Edit.tsx b/services/frontend/src/components/modals/kubernetes/service/Edit.tsx new file mode 100644 index 0000000..9815704 --- /dev/null +++ b/services/frontend/src/components/modals/kubernetes/service/Edit.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import type { CallbackFunction, IServiceNodeItem } from "../../../../types"; +import type { IEditServiceForm } from "../../../../types/docker-compose"; +import { + getInitialValues, + getFinalValues, + validationSchema, + tabs +} from "./form-utils"; +import { toaster } from "../../../../utils"; +import FormModal from "../../../FormModal"; + +export interface IModalServiceProps { + node: IServiceNodeItem; + onHide: CallbackFunction; + onUpdateEndpoint: CallbackFunction; +} + +const ModalServiceEdit = (props: IModalServiceProps) => { + const { node, onHide, onUpdateEndpoint } = props; + const [selectedNode, setSelectedNode] = useState(); + + useEffect(() => { + if (node) { + setSelectedNode(node); + } + }, [node]); + + const handleUpdate = ( + finalValues: IServiceNodeItem, + values: IEditServiceForm + ) => { + onUpdateEndpoint(finalValues); + toaster(`Updated "${values.serviceName}" service successfully`, "success"); + }; + + return ( + + ); +}; + +export default ModalServiceEdit; diff --git a/services/frontend/src/components/modals/kubernetes/service/General.tsx b/services/frontend/src/components/modals/kubernetes/service/General.tsx new file mode 100644 index 0000000..020a2c0 --- /dev/null +++ b/services/frontend/src/components/modals/kubernetes/service/General.tsx @@ -0,0 +1,295 @@ +import { styled } from "@mui/joy"; +import { TFinalFormField } from "../../../../types"; +import { SuperForm } from "../../../SuperForm"; + +const Root = styled("div")` + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(1)}; + @media (max-width: 640px) { + row-gap: 0; + } +`; + +const General = () => { + return ( + + [ + { + id: `ports[${index}].hostPort`, + type: "text", + name: `ports[${index}].hostPort`, + placeholder: "Host port", + required: true + }, + { + id: `ports[${index}].containerPort`, + type: "text", + name: `ports[${index}].containerPort`, + placeholder: "Container port" + }, + { + id: `ports[${index}].protocol`, + type: "toggle", + name: `ports[${index}].protocol`, + label: "Protocol", + options: [ + { + value: "tcp", + text: "TCP" + }, + { + value: "udp", + text: "UDP" + } + ] + } + ], + newValue: { + hostPort: "", + containerPort: "", + protocol: "" + } + }, + { + id: "dependsOn", + type: "records", + name: "dependsOn", + title: "Depends on", + fields: (index: number): TFinalFormField[] => [ + { + id: `dependsOn[${index}]`, + type: "text", + name: `dependsOn[${index}]`, + placeholder: "Service name", + required: false + } + ], + newValue: "" + }, + { + id: "networks", + type: "records", + title: "Networks", + name: "networks", + fields: (index: number): TFinalFormField[] => [ + { + id: `networks[${index}]`, + type: "text", + name: `networks[${index}]`, + placeholder: "Network name", + required: false + } + ], + newValue: "" + }, + { + id: "labels", + type: "records", + title: "Labels", + name: "labels", + fields: (index: number): TFinalFormField[] => [ + { + id: `labels[${index}].key`, + type: "text", + name: `labels[${index}].key`, + placeholder: "Key", + required: true + }, + { + id: `labels[${index}].value`, + type: "text", + name: `labels[${index}].value`, + placeholder: "Value", + required: true + } + ], + newValue: { key: "", value: "" } + }, + { + id: "profiles", + type: "records", + title: "Profiles", + name: "profiles", + fields: (index: number): TFinalFormField[] => [ + { + id: `profiles[${index}]`, + name: `profiles[${index}]`, + placeholder: "Name", + required: true, + type: "text" + } + ], + newValue: "" + } + ]} + /> + + ); +}; + +export default General; diff --git a/services/frontend/src/components/modals/kubernetes/service/form-utils.ts b/services/frontend/src/components/modals/kubernetes/service/form-utils.ts new file mode 100644 index 0000000..9de59a5 --- /dev/null +++ b/services/frontend/src/components/modals/kubernetes/service/form-utils.ts @@ -0,0 +1,55 @@ +import type { IServiceNodeItem } from "../../../../types"; +import type { IEditServiceForm } from "../../../../types/kubernetes"; +import * as yup from "yup"; +import General from "./General"; + +export const tabs = [ + { + value: "general", + title: "General", + component: General + } +]; + +const initialValues: IEditServiceForm = { + serviceName: "" +}; + +export const validationSchema = yup.object({ + serviceName: yup + .string() + .max(256, "Service name should be 256 characters or less") + .required("Service name is required") +}); + +export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { + if (!node) { + return { + ...initialValues + }; + } + + const { canvasConfig } = node; + const { node_name = "" } = canvasConfig; + + return { + serviceName: node_name + }; +}; + +export const getFinalValues = ( + values: IEditServiceForm, + previous?: IServiceNodeItem +): IServiceNodeItem => { + return { + key: previous?.key ?? "service", + type: "SERVICE", + position: previous?.position ?? { left: 0, top: 0 }, + inputs: previous?.inputs ?? ["op_source"], + outputs: previous?.outputs ?? [], + canvasConfig: { + node_name: values.serviceName + }, + serviceConfig: {} + }; +}; diff --git a/services/frontend/src/types/docker-compose/index.ts b/services/frontend/src/types/docker-compose/index.ts new file mode 100644 index 0000000..bf699ff --- /dev/null +++ b/services/frontend/src/types/docker-compose/index.ts @@ -0,0 +1,441 @@ +import { KeyValuePair, INodeItem, ICanvasConfig } from "../index"; + +export interface IVolumeTopLevel { + driver: string; + driver_opts: { + type: string; + o: string; + device: string; + }; + external: boolean; + labels?: string[] | KeyValuePair; + name: string; +} + +export interface IPAMConfig { + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: Record; +} + +export interface IIPAM { + driver?: string; + config?: IPAMConfig[]; + options?: Record; +} + +export interface INetworkTopLevel { + driver: string; + driver_opts: KeyValuePair; + attachable: boolean; + enable_ipv6: boolean; + ipam?: IIPAM; + internal: boolean; + labels?: string[] | KeyValuePair; + external: boolean; + name: string; +} + +export interface IService { + build?: { + context: string; + dockerfile?: string; + args?: string[] | KeyValuePair; + ssh?: string[]; + cache_from?: string[]; + cache_to?: string[]; + extra_hosts?: string[]; + isolation?: string; + labels?: string[] | KeyValuePair; + shm_size?: string | number; + target?: string; + }; + cpu_count: string | number; + cpu_percent: string | number; + cpu_shares: string | number; + cpu_period: string | number; + cpu_quota: string | number; + cpu_rt_runtime: string | number; + cpu_rt_period: string | number; + cpuset: number | number[]; + cap_add: string[]; + cap_drop: string[]; + cgroup_parent: string; + command: string | string[]; + configs: + | string[] + | { + [x: string]: { + source: string; + target: string; + uid: string; + gid: string; + mode: number; + }; + }; + container_name: string; + credential_spec: KeyValuePair; + depends_on: + | string[] + | { + [key: string]: { + condition: string; + }; + }; + deploy?: { + endpoint_mode?: "vip" | "dnsrr"; + labels?: string[] | KeyValuePair; + mode?: "replicated" | "global"; + placement?: { + constraints?: KeyValuePair[] | KeyValuePair; + preferences?: KeyValuePair[] | KeyValuePair; + }; + replicas?: number; + resources?: { + limits?: { + cpus?: string; + memory?: string; + pids?: number; + }; + reservations?: { + cpus?: string; + memory?: string; + devices?: { [key: string]: string | number | string[] }[]; + }; + }; + restart_policy?: { + condition?: "none" | "on-failure" | "any"; + delay?: string; + max_attempts?: number; + window?: string; + }; + rollback_config?: { + parallelism?: number; + delay?: string; + failure_action?: "continue" | "pause"; + monitor?: string; + max_failure_ratio?: string; + order?: "stop-first" | "start-first"; + }; + update_config?: { + parallelism?: number; + delay?: string; + failure_action?: "continue" | "pause"; + monitor?: string; + max_failure_ratio?: string; + order?: "stop-first" | "start-first"; + }; + }; + device_cgroup_rules: string[]; + devices: string[]; + dns: string | string[]; + dns_opt: string[]; + dns_search: string | string[]; + domainname: string; + entrypoint: string | string[]; + env_file: string | string[]; + environment: string[] | KeyValuePair; + expose: string[]; + extends: KeyValuePair; + external_links: string[]; + extra_hosts: string[]; + group_add: string[]; + healthcheck: { + test: string[]; + interval: string; + timeout: string; + retries: number; + start_period: string; + }; + hostname: string; + image: string; + init: boolean; + ipc: string; + isolation: string; + labels: string[] | KeyValuePair; + links: string[]; + logging: { + driver: string; + options: KeyValuePair; + }; + network_mode: string; + networks: + | string[] + | { + [x: string]: { + aliases: string[]; + ipv4_address: string; + ipv6_address: string; + link_local_ips: string[]; + priority: number; + }; + }; + mac_address: string; + mem_swappiness: number; + memswap_limit: string | number; + oom_kill_disable: boolean; + oom_score_adj: number; + pid: string | number; + platform: string; + ports: + | string[] + | { + target: number; + host_ip: string; + published: string | number; + protocol: string; + mode: string; + }; + privileged: boolean; + profiles?: string[]; + pull_policy: string; + read_only: boolean; + restart: string; + runtime: string; + secrets: + | string[] + | { + source: string; + target: string; + uid: string; + gid: string; + mode: number; + }; + security_opt: string[]; + shm_size: string; + stdin_open: boolean; + stop_grace_period: string; + stop_signal: string; + storage_opt: { + size: string; + }; + sysctls: string[] | KeyValuePair; + tmpfs: string | string[]; + tty: boolean; + ulimits: { + nproc: number; + nofile: { + soft: number; + hard: number; + }; + }; + user: string; + userns_mode: string; + volumes: + | string[] + | { + type: string; + source: string; + target: string; + read_only: boolean; + bind: { + propagation: string; + create_host_path: boolean; + selinux: string; + }; + volume: { + nocopy: boolean; + }; + tmpfs: { + size: string | number; + }; + consistency: string; + }; + volumes_from: string[]; + working_dir: string; +} + +export interface IVolumeNodeItem extends INodeItem { + outputs: string[]; + canvasConfig: ICanvasConfig; + volumeConfig: Partial; +} + +export interface INetworkNodeItem extends INodeItem { + outputs: string[]; + canvasConfig: ICanvasConfig; + networkConfig: Partial; +} + +export interface IProjectPayload { + name: string; + visibility: number; + project_type: number; + data: { + canvas: { + position: { + top: number; + left: number; + scale: number; + }; + nodes: any; + connections: any; + networks: any; + }; + }; +} + +export interface IGeneratePayload { + data: { + version: number; + networks: Record>; + services: Record>; + volumes: Record>; + }; +} + +export interface IEditServiceForm { + build: { + context: string; + dockerfile: string; + arguments: { + key: string[]; + value: string[]; + }[]; + sshAuthentications: { + id: string; + path: string; + }[]; + cacheFrom: string[]; + cacheTo: string[]; + extraHosts: { + hostName: string; + ipAddress: string; + }[]; + isolation: string; + labels: { + key: string[]; + value: string[]; + }[]; + sharedMemorySize: string; + target: string; + }; + command: string; + deploy: { + /** + * The default value for `mode` is `replicated`. However, we allow + * it to be empty. Thus, `mode` attribute can be pruned away + * if the user never assigned `mode` explicitly. + */ + mode: "" | "global" | "replicated"; + /** + * The default value for `endpointMode` is platform dependent. + */ + endpointMode: "" | "vip" | "dnsrr"; + replicas: string; + placement: { + constraints: { + key: string; + value: string; + }[]; + preferences: { + key: string; + value: string; + }[]; + }; + resources: { + limits: { + cpus: string; + memory: string; + pids: string; + }; + reservations: { + cpus: string; + memory: string; + }; + }; + restartPolicy: { + /** + * The default value for `condition` is `any`. However, we allow + * it to be empty. Thus, `deploy` attribute can be pruned away + * if the user never assigned `condition` explicitly. + */ + condition: "" | "none" | "on-failure" | "any"; + delay: string; + maxAttempts: string; + window: string; + }; + rollbackConfig: { + parallelism: string; + delay: string; + failureAction: "" | "continue" | "pause"; + monitor: string; + maxFailureRatio: string; + order: "" | "stop-first" | "start-first"; + }; + updateConfig: { + parallelism: string; + delay: string; + failureAction: "" | "continue" | "pause"; + monitor: string; + maxFailureRatio: string; + order: "" | "stop-first" | "start-first"; + }; + labels: { + key: string; + value: string; + }[]; + }; + entrypoint: string; + envFile: string; + serviceName: string; + imageName: string; + imageTag: string; + containerName: string; + networks: string[]; + profiles: string[]; + ports: { + hostPort: string; + containerPort: string; + protocol: "tcp" | "udp"; + }[]; + environmentVariables: { + key: string; + value: string; + }[]; + restart: string; + volumes: { + name: string; + containerPath: string; + accessMode: string; + }[]; + labels: { + key: string; + value: string; + }[]; + dependsOn: string[]; + workingDir: string; +} + +export interface IEditVolumeForm { + entryName: string; + volumeName: string; + labels: { + key: string; + value: string; + }[]; +} + +export interface IEditNetworkForm { + entryName: string; + networkName: string; + driver: string; + configurations: { + subnet: string; + ipRange: string; + gateway: string; + auxAddresses: { + hostName: string; + ipAddress: string; + }[]; + }[]; + options: { + key: string; + value: string; + }[]; + labels: { + key: string; + value: string; + }[]; +} diff --git a/services/frontend/src/types/kubernetes/index.ts b/services/frontend/src/types/kubernetes/index.ts new file mode 100644 index 0000000..7525c97 --- /dev/null +++ b/services/frontend/src/types/kubernetes/index.ts @@ -0,0 +1,30 @@ +export interface IService { + container_name: string; +} + +export interface IGeneratePayload { + data: { + services: Record>; + }; +} + +export interface IKubernetesProjectPayload { + name: string; + visibility: number; + project_type: number; + data: { + canvas: { + position: { + top: number; + left: number; + scale: number; + }; + nodes: any; + connections: any; + }; + }; +} + +export interface IEditServiceForm { + serviceName: string; +}