Skip to content

Commit 56714b3

Browse files
committed
feat(front): ephemeral environment CRUD actions
1 parent 87d8bff commit 56714b3

30 files changed

+1674
-76
lines changed

front/assets/.eslintrc.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module.exports = {
2626
},
2727
ignorePatterns: ["*.js", "*.json"],
2828
rules: {},
29-
overrides: [
29+
overrides: [
3030
{
3131
files: ["js/**/*.spec.js"],
3232
env: {
@@ -86,7 +86,7 @@ module.exports = {
8686
},
8787
},
8888
],
89-
"no-console": 1,
89+
"no-console": ["error", { allow: ["warn", "error"] }],
9090
"no-multi-spaces": "error",
9191
"@typescript-eslint/member-delimiter-style": [
9292
"warn",
@@ -114,6 +114,12 @@ module.exports = {
114114
beforeClosing: "never",
115115
},
116116
],
117+
"@typescript-eslint/no-misused-promises": [
118+
"error",
119+
{
120+
"checksVoidReturn": false
121+
}
122+
]
117123
},
118124
},
119125
],

front/assets/js/app.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { default as AddPeople } from "./people/add_people";
6767
import { default as EditPerson } from "./people/edit_person";
6868
import { default as SyncPeople } from "./people/sync_people";
6969
import { default as ServiceAccounts } from "./service_accounts";
70+
import { default as EphemeralEnvironments } from "./ephemeral_environments";
7071
import { default as Report } from "./report";
7172

7273
import { InitializingScreen } from "./project_onboarding/initializing";
@@ -287,6 +288,13 @@ export var App = {
287288
});
288289
new Star();
289290
},
291+
ephemeral_environments_page: function () {
292+
const ephemeralEnvironmentsEl = document.getElementById("ephemeral-environments");
293+
if (ephemeralEnvironmentsEl) {
294+
const config = JSON.parse(ephemeralEnvironmentsEl.dataset.config);
295+
EphemeralEnvironments({ dom: ephemeralEnvironmentsEl, config });
296+
}
297+
},
290298
people_page: function () {
291299
ListPeople.init();
292300
ChangeRoleDropdown.init();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Routes, Route } from "react-router-dom";
2+
import { EnvironmentsListPage } from "./pages/EnvironmentsListPage";
3+
import { CreateEnvironmentPage } from "./pages/CreateEnvironmentPage";
4+
import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage";
5+
import { EditEnvironmentPage } from "./pages/EditEnvironmentPage";
6+
7+
export const App = () => {
8+
return (
9+
<Routes>
10+
<Route path="" element={<EnvironmentsListPage/>}/>
11+
<Route path="/new" element={<CreateEnvironmentPage/>}/>
12+
<Route path="/:id" element={<EnvironmentDetailsPage/>}/>
13+
<Route path="/:id/edit" element={<EditEnvironmentPage/>}/>
14+
</Routes>
15+
);
16+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Link } from "react-router-dom";
2+
import { EnvironmentType, InstanceCounts } from "../types";
3+
import { MaterializeIcon, Tooltip } from "js/toolbox";
4+
5+
interface EnvironmentCardProps {
6+
environment: EnvironmentType;
7+
counts?: InstanceCounts;
8+
onClick?: () => void;
9+
}
10+
11+
export const EnvironmentCard = ({ environment, counts, onClick }: EnvironmentCardProps) => {
12+
const getStatusColor = (state: EnvironmentType[`state`]) => {
13+
switch (state) {
14+
case `ready`:
15+
return `green`;
16+
case `draft`:
17+
return `gray`;
18+
case `cordoned`:
19+
return `gold`;
20+
case `deleted`:
21+
return `red`;
22+
default:
23+
return `gray`;
24+
}
25+
};
26+
27+
const getInstanceBar = () => {
28+
if (!counts) return null;
29+
30+
const { pending, running, failed, total } = counts;
31+
const maxInstances = environment.max_number_of_instances;
32+
33+
return (
34+
<div className="mt2">
35+
<div className="flex justify-between f7 mb1">
36+
<span>pending</span>
37+
<span className="gold">{pending}/{maxInstances}</span>
38+
</div>
39+
<div className="w-100 bg-black-10 br2 overflow-hidden" style={{ height: `4px` }}>
40+
<div
41+
className="bg-gold h-100"
42+
style={{ width: `${(pending / maxInstances) * 100}%` }}
43+
/>
44+
</div>
45+
46+
<div className="flex justify-between f7 mb1 mt2">
47+
<span>running</span>
48+
<span className="green">{running}/{maxInstances}</span>
49+
</div>
50+
<div className="w-100 bg-black-10 br2 overflow-hidden" style={{ height: `4px` }}>
51+
<div
52+
className="bg-green h-100"
53+
style={{ width: `${(running / maxInstances) * 100}%` }}
54+
/>
55+
</div>
56+
57+
{failed > 0 && (
58+
<>
59+
<div className="flex justify-between f7 mb1 mt2">
60+
<span>failed</span>
61+
<span className="red">{failed}/{maxInstances}</span>
62+
</div>
63+
<div className="w-100 bg-black-10 br2 overflow-hidden" style={{ height: `4px` }}>
64+
<div
65+
className="bg-red h-100"
66+
style={{ width: `${(failed / maxInstances) * 100}%` }}
67+
/>
68+
</div>
69+
</>
70+
)}
71+
</div>
72+
);
73+
};
74+
75+
const hasFailedProvisioning = counts && counts.failed > 0;
76+
77+
return (
78+
<Link
79+
to={`/${environment.id}`}
80+
className="db bg-white ba b--black-10 br3 pa3 pointer hover-shadow-1 transition-shadow link black"
81+
>
82+
<div className="flex items-start justify-between mb2">
83+
<h3 className="f5 ma0 lh-title">{environment.name}</h3>
84+
{hasFailedProvisioning && (
85+
<Tooltip
86+
content={`There is one failed provisioning pipeline`}
87+
placement="top"
88+
anchor={
89+
<MaterializeIcon
90+
name="info"
91+
className="f5 red pointer"
92+
/>}
93+
/>
94+
)}
95+
</div>
96+
97+
{environment.description && (
98+
<p className="f6 gray mt1 mb2 lh-copy">{environment.description}</p>
99+
)}
100+
101+
<div className="flex items-center mb2">
102+
<span className={`f7 fw5 ${getStatusColor(environment.state)}`}>
103+
{environment.state.toUpperCase()}
104+
</span>
105+
</div>
106+
107+
{getInstanceBar()}
108+
</Link>
109+
);
110+
};
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { useState } from "preact/hooks";
2+
import { Link } from "react-router-dom";
3+
import { EnvironmentDetails as EnvironmentDetailsType, EnvironmentInstance } from "../types";
4+
import { MaterializeIcon, Box, Formatter } from "js/toolbox";
5+
6+
interface EnvironmentDetailsProps {
7+
environment: EnvironmentDetailsType;
8+
onBack: () => void;
9+
onEdit: () => void;
10+
onDelete: () => void;
11+
onProvision: () => void;
12+
onDeprovision: (instanceId: string) => void;
13+
canManage: boolean;
14+
}
15+
16+
export const EnvironmentDetails = ({
17+
environment,
18+
onBack,
19+
onEdit,
20+
onDelete,
21+
onProvision,
22+
onDeprovision,
23+
canManage
24+
}: EnvironmentDetailsProps) => {
25+
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
26+
27+
const getInstanceStatusIcon = (state: EnvironmentInstance[`state`]) => {
28+
if (state === `ready_to_use` || state === `in_use`) {
29+
return <span className="dib w1 h1 br-100 bg-green mr2"/>;
30+
} else if (state === `provisioning` || state === `deploying`) {
31+
return <span className="dib w1 h1 br-100 bg-gold mr2"/>;
32+
} else if (state.startsWith(`failed`)) {
33+
return <span className="dib w1 h1 br-100 bg-red mr2"/>;
34+
}
35+
return <span className="dib w1 h1 br-100 bg-gray mr2"/>;
36+
};
37+
38+
const getInstanceStatusText = (state: EnvironmentInstance[`state`]) => {
39+
return state.replace(/_/g, ` `);
40+
};
41+
42+
return (
43+
<div className="bg-washed-gray mt4 pa3 pa4-l br3 ba b--black-075">
44+
<div className="mw8 center">
45+
<nav className="mb4">
46+
<ol className="list ma0 pa0 f6">
47+
<li className="dib mr2">
48+
<Link
49+
to="/"
50+
className="pointer flex items-center f6"
51+
>
52+
Environments
53+
</Link>
54+
</li>
55+
<li className="dib mr2 gray">
56+
/
57+
</li>
58+
<li className="dib gray">
59+
{environment.name}
60+
</li>
61+
</ol>
62+
</nav>
63+
64+
<div className="bg-white ba b--black-10 br3 pa4 mb4">
65+
<div className="flex items-start justify-between mb3">
66+
<div>
67+
<h2 className="f3 ma0 mb2">{environment.name}</h2>
68+
{environment.description && (
69+
<p className="f5 gray ma0 mb3">{environment.description}</p>
70+
)}
71+
<div className="flex items-center">
72+
<span className="f6 gray mr3">
73+
Last modified by {environment.last_updated_by} on {` `}
74+
{new Date(environment.updated_at).toLocaleDateString(`en-US`, {
75+
year: `numeric`,
76+
month: `short`,
77+
day: `numeric`
78+
})}
79+
</span>
80+
</div>
81+
</div>
82+
{canManage && (
83+
<div className="flex">
84+
<button
85+
className="btn btn-secondary mr2"
86+
onClick={onEdit}
87+
>
88+
Edit
89+
</button>
90+
<button
91+
className="btn btn-secondary"
92+
onClick={onDelete}
93+
>
94+
Delete
95+
</button>
96+
</div>
97+
)}
98+
</div>
99+
100+
<div className="bt b--black-10 pt3">
101+
<div className="flex items-center justify-between mb3">
102+
<p className="f6 gray ma0">
103+
Available to use in: Saas, alles, semaphore, alex_test_project and 11 more projects
104+
</p>
105+
{canManage && (
106+
<button
107+
className="btn btn-primary"
108+
onClick={onProvision}
109+
>
110+
Provision new instance
111+
</button>
112+
)}
113+
</div>
114+
115+
{(!environment.instances || environment.instances.length === 0) ? (
116+
<Box type="info" className="mt3">
117+
<p className="ma0">No instances provisioned yet. Click &quot;Provision new instance&quot; to create one.</p>
118+
</Box>
119+
) : (
120+
<div className="mt3">
121+
<table className="w-100">
122+
<tbody>
123+
{environment.instances.map(instance => (
124+
<tr key={instance.id} className="bb b--black-10">
125+
<td className="pv3 pr3 w2">
126+
{getInstanceStatusIcon(instance.state)}
127+
</td>
128+
<td className="pv3 pr3">
129+
<div>
130+
{instance.url ? (
131+
<a
132+
href={instance.url}
133+
target="_blank"
134+
rel="noopener noreferrer"
135+
className="link blue hover-dark-blue fw5"
136+
>
137+
{instance.name}
138+
</a>
139+
) : (
140+
<span className="fw5">{instance.name}</span>
141+
)}
142+
{instance.url && (
143+
<div className="f7 gray mt1">{instance.url}</div>
144+
)}
145+
</div>
146+
</td>
147+
<td className="pv3 pr3 f6 gray">
148+
{instance.state === `ready_to_use` && instance.provisioned_at && (
149+
<div>
150+
provisioned on {new Date(instance.provisioned_at).toLocaleDateString()} by amir
151+
<br/>
152+
last deployed to {Formatter.formatTimeAgo(instance.updated_at)} by {instance.deployed_by || `unknown`}
153+
</div>
154+
)}
155+
{instance.state === `provisioning` && (
156+
<div className="gold">provisioning</div>
157+
)}
158+
{instance.state.startsWith(`failed`) && (
159+
<div className="red">{getInstanceStatusText(instance.state)}</div>
160+
)}
161+
</td>
162+
<td className="pv3 tc">
163+
{canManage && (
164+
<>
165+
{confirmDelete === instance.id ? (
166+
<div className="flex items-center">
167+
<span className="f6 mr2">Are you sure?</span>
168+
<button
169+
className="btn btn-tiny btn-danger mr1"
170+
onClick={() => {
171+
onDeprovision(instance.id);
172+
setConfirmDelete(null);
173+
}}
174+
>
175+
Yes
176+
</button>
177+
<button
178+
className="btn btn-tiny btn-secondary"
179+
onClick={() => setConfirmDelete(null)}
180+
>
181+
No
182+
</button>
183+
</div>
184+
) : (
185+
<button
186+
className="btn btn-secondary"
187+
onClick={() => setConfirmDelete(instance.id)}
188+
disabled={instance.state === `provisioning`}
189+
>
190+
{instance.state === `provisioning` ? `Provisioning...` :
191+
instance.state.startsWith(`failed`) ? `Dismiss` : `Deprovision`}
192+
</button>
193+
)}
194+
</>
195+
)}
196+
</td>
197+
</tr>
198+
))}
199+
</tbody>
200+
</table>
201+
</div>
202+
)}
203+
</div>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
};

0 commit comments

Comments
 (0)