Skip to content
This repository was archived by the owner on Jun 18, 2025. It is now read-only.

Commit 1f27c6b

Browse files
feat: add deployment resource editor (#176)
This change adds the Deployment resource editor.
1 parent b48a7fa commit 1f27c6b

32 files changed

+4429
-0
lines changed

plugins/cad/src/components/Controls/Autocomplete.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ type AutocompleteProps = {
2323
options: string[];
2424
value: string;
2525
onInputChange: (newValue: string) => void;
26+
allowArbitraryValues?: boolean;
2627
};
2728

2829
export const Autocomplete = ({
2930
label,
3031
options,
3132
value,
3233
onInputChange,
34+
allowArbitraryValues,
3335
}: AutocompleteProps) => {
3436
const thisValue = useRef<string>(value);
3537
const inputValue = useRef<string>(value);
@@ -59,6 +61,7 @@ export const Autocomplete = ({
5961
return (
6062
<MaterialAutocomplete
6163
fullWidth
64+
freeSolo={allowArbitraryValues ?? false}
6265
options={options}
6366
renderInput={params => (
6467
<TextField {...params} label={label} variant="outlined" fullWidth />

plugins/cad/src/components/ResourceEditorDialog/components/FirstClassEditorSelector.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React, { useEffect, useRef } from 'react';
1818
import { PackageResource } from '../../../utils/packageRevisionResources';
1919
import { ApplyReplacementsEditor } from './FirstClassEditors/ApplyReplacementsEditor';
2020
import { ConfigMapEditor } from './FirstClassEditors/ConfigMapEditor';
21+
import { DeploymentEditor } from './FirstClassEditors/DeploymentEditor';
2122
import { IngressEditor } from './FirstClassEditors/IngressEditor';
2223
import { KptfileEditor } from './FirstClassEditors/KptfileEditor';
2324
import { NamespaceEditor } from './FirstClassEditors/NamespaceEditor';
@@ -58,6 +59,15 @@ export const FirstClassEditorSelector = ({
5859
}, [onNoNamedEditor]);
5960

6061
switch (groupVersionKind) {
62+
case 'apps/v1/Deployment':
63+
return (
64+
<DeploymentEditor
65+
yaml={yaml}
66+
onUpdatedYaml={onUpdatedYaml}
67+
packageResources={packageResources}
68+
/>
69+
);
70+
6171
case 'fn.kpt.dev/v1alpha1/ApplyReplacements':
6272
return (
6373
<ApplyReplacementsEditor
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Button } from '@material-ui/core';
18+
import AddIcon from '@material-ui/icons/Add';
19+
import { cloneDeep } from 'lodash';
20+
import React, { useEffect, useState } from 'react';
21+
import {
22+
Deployment,
23+
DeploymentMetadata,
24+
DeploymentStrategy,
25+
LabelSelector,
26+
} from '../../../../../types/Deployment';
27+
import {
28+
Container,
29+
PodSecurityContext,
30+
Volume,
31+
} from '../../../../../types/Pod';
32+
import { PackageResource } from '../../../../../utils/packageRevisionResources';
33+
import { dumpYaml, loadYaml } from '../../../../../utils/yaml';
34+
import { ResourceMetadataAccordion } from '../Controls/ResourceMetadataAccordion';
35+
import { useEditorStyles } from '../styles';
36+
import {
37+
Deletable,
38+
getActiveElements,
39+
isActiveElement,
40+
updateList,
41+
} from '../util/deletable';
42+
import { ContainerEditorAccordion } from './components/ContainerEditorAccordion';
43+
import { DeploymentDetailsEditorAccordion } from './components/DeploymentDetailsEditorAccordion';
44+
import { PodDetailsEditorAccordion } from './components/PodDetailsEditorAccordion';
45+
46+
type OnUpdatedYamlFn = (yaml: string) => void;
47+
48+
type DeploymentEditorProps = {
49+
yaml: string;
50+
onUpdatedYaml: OnUpdatedYamlFn;
51+
packageResources: PackageResource[];
52+
};
53+
54+
type State = {
55+
metadata: DeploymentMetadata;
56+
selector: LabelSelector;
57+
replicas?: number;
58+
strategy?: DeploymentStrategy;
59+
minReadySeconds?: number;
60+
progressDeadlineSeconds?: number;
61+
revisionHistoryLimit?: number;
62+
volumes: Volume[];
63+
serviceAccount?: string;
64+
securityContext?: PodSecurityContext;
65+
restartPolicy?: string;
66+
terminationGracePeriodSeconds?: number;
67+
};
68+
69+
const getResourceState = (deployment: Deployment): State => {
70+
deployment.spec = deployment.spec || { replicas: 1 };
71+
72+
const deploymentSpec = deployment.spec;
73+
74+
deploymentSpec.selector = deploymentSpec.selector || {
75+
matchLabels: { 'app.kubernetes.io/name': 'app' },
76+
};
77+
deploymentSpec.template = deploymentSpec.template || {};
78+
deploymentSpec.template.metadata = deployment.spec.template.metadata || {};
79+
deploymentSpec.template.spec = deploymentSpec.template.spec || {
80+
containers: [{ name: 'main' }],
81+
};
82+
83+
const templateSpec = deployment.spec.template.spec;
84+
85+
return {
86+
metadata: deployment.metadata,
87+
selector: deploymentSpec.selector,
88+
replicas: deploymentSpec.replicas,
89+
strategy: deploymentSpec.strategy,
90+
minReadySeconds: deploymentSpec.minReadySeconds,
91+
progressDeadlineSeconds: deploymentSpec.progressDeadlineSeconds,
92+
revisionHistoryLimit: deploymentSpec.revisionHistoryLimit,
93+
volumes: templateSpec.volumes ?? [],
94+
serviceAccount: templateSpec.serviceAccountName,
95+
securityContext: templateSpec.securityContext,
96+
restartPolicy: templateSpec.restartPolicy,
97+
terminationGracePeriodSeconds: templateSpec.terminationGracePeriodSeconds,
98+
};
99+
};
100+
101+
export const DeploymentEditor = ({
102+
yaml,
103+
onUpdatedYaml,
104+
packageResources,
105+
}: DeploymentEditorProps) => {
106+
const resourceYaml = loadYaml(yaml) as Deployment;
107+
108+
const classes = useEditorStyles();
109+
110+
const [state, setState] = useState<State>(getResourceState(resourceYaml));
111+
const [expanded, setExpanded] = useState<string>();
112+
const [containers, setContainers] = useState<Deletable<Container>[]>(
113+
resourceYaml.spec.template.spec.containers ?? [],
114+
);
115+
116+
useEffect(() => {
117+
resourceYaml.metadata = state.metadata;
118+
119+
const spec = resourceYaml.spec;
120+
spec.replicas = state.replicas;
121+
spec.strategy = state.strategy;
122+
spec.minReadySeconds = state.minReadySeconds;
123+
spec.progressDeadlineSeconds = state.progressDeadlineSeconds;
124+
spec.revisionHistoryLimit = state.revisionHistoryLimit;
125+
spec.selector = cloneDeep(state.selector);
126+
127+
const templateSpec = spec.template.spec;
128+
spec.template.metadata.labels = cloneDeep(state.selector.matchLabels);
129+
templateSpec.containers = getActiveElements(containers);
130+
templateSpec.volumes = state.volumes.length > 0 ? state.volumes : undefined;
131+
templateSpec.serviceAccountName = state.serviceAccount;
132+
templateSpec.securityContext = state.securityContext;
133+
templateSpec.restartPolicy = state.restartPolicy;
134+
templateSpec.terminationGracePeriodSeconds =
135+
state.terminationGracePeriodSeconds;
136+
137+
onUpdatedYaml(dumpYaml(resourceYaml));
138+
}, [state, onUpdatedYaml, resourceYaml, containers]);
139+
140+
return (
141+
<div className={classes.root}>
142+
<ResourceMetadataAccordion
143+
id="metadata"
144+
state={[expanded, setExpanded]}
145+
value={state.metadata}
146+
onUpdate={metadata => setState(s => ({ ...s, metadata }))}
147+
/>
148+
149+
<DeploymentDetailsEditorAccordion
150+
id="deployment-details"
151+
state={[expanded, setExpanded]}
152+
value={state}
153+
onUpdate={updatedState => setState(s => ({ ...s, ...updatedState }))}
154+
/>
155+
156+
<PodDetailsEditorAccordion
157+
id="pod-details"
158+
state={[expanded, setExpanded]}
159+
value={state}
160+
onUpdate={updatedState => setState(s => ({ ...s, ...updatedState }))}
161+
packageResources={packageResources}
162+
/>
163+
164+
{containers.map(
165+
(thisContainer, index) =>
166+
isActiveElement(thisContainer) && (
167+
<ContainerEditorAccordion
168+
id={`container-${index}`}
169+
key={`container-${index}`}
170+
state={[expanded, setExpanded]}
171+
value={thisContainer}
172+
volumes={state.volumes}
173+
packageResources={packageResources}
174+
onUpdate={updatedContainer => {
175+
setContainers(
176+
updateList(containers.slice(), updatedContainer, index),
177+
);
178+
}}
179+
/>
180+
),
181+
)}
182+
183+
<div className={classes.buttonRow}>
184+
<Button
185+
variant="outlined"
186+
startIcon={<AddIcon />}
187+
onClick={() => {
188+
setContainers([...containers, { name: '' }]);
189+
setExpanded(`container-${containers.length}`);
190+
}}
191+
>
192+
Add Container
193+
</Button>
194+
</div>
195+
</div>
196+
);
197+
};

0 commit comments

Comments
 (0)