Skip to content

Commit 31742b3

Browse files
committed
[dashboard] env variables
1 parent 0912083 commit 31742b3

File tree

2 files changed

+189
-6
lines changed

2 files changed

+189
-6
lines changed

components/dashboard/src/settings/EnvironmentVariables.tsx

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,195 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7+
import { UserEnvVarValue } from "@gitpod/gitpod-protocol";
8+
import { useEffect, useRef, useState } from "react";
9+
import ContextMenu from "../components/ContextMenu";
10+
import Modal from "../components/Modal";
11+
import { getGitpodService } from "../service/service";
712
import { SettingsPage } from "./SettingsPage";
13+
import ThreeDots from '../icons/ThreeDots.svg';
14+
15+
interface EnvVarModalProps {
16+
envVar: UserEnvVarValue;
17+
onClose: () => void;
18+
save: (v: UserEnvVarValue) => void;
19+
validate: (v: UserEnvVarValue) => string;
20+
}
21+
22+
function AddEnvVarModal(p: EnvVarModalProps) {
23+
const [ev, setEv] = useState({...p.envVar});
24+
const [error, setError] = useState('');
25+
const ref = useRef(ev);
26+
27+
const update = (pev: Partial<UserEnvVarValue>) => {
28+
const newEnv = { ...ref.current, ... pev};
29+
setEv(newEnv);
30+
ref.current = newEnv;
31+
};
32+
33+
useEffect(() => {
34+
setEv({...p.envVar});
35+
setError('');
36+
}, [p.envVar]);
37+
38+
const isNew = !p.envVar.id;
39+
let save = () => {
40+
const v = ref.current;
41+
const errorMsg = p.validate(v);
42+
if (errorMsg !== '') {
43+
setError(errorMsg);
44+
return false;
45+
} else {
46+
p.save(v);
47+
p.onClose();
48+
return true;
49+
}
50+
};
51+
52+
return <Modal visible={true} onClose={p.onClose} onEnter={save}>
53+
<h3 className="pb-2">{isNew ? 'New' : 'Edit'} Variable</h3>
54+
<div className="border-t -mx-6 px-6 py-2 flex flex-col">
55+
{error ? <div className="bg-gitpod-kumquat-light rounded-md p-3 text-red-500 text-sm">
56+
{error}
57+
</div> : null}
58+
<div className="mt-4">
59+
<h4>Name</h4>
60+
<input className="w-full" type="text" value={ev.name} onChange={(v) => { update({name: v.target.value}) }} />
61+
</div>
62+
<div className="mt-4">
63+
<h4>Value</h4>
64+
<input className="w-full" type="text" value={ev.value} onChange={(v) => { update({value: v.target.value}) }} />
65+
</div>
66+
<div className="mt-4">
67+
<h4>Scope</h4>
68+
<input className="w-full" type="text" value={ev.repositoryPattern} placeholder="org/project"
69+
onChange={(v) => { update({repositoryPattern: v.target.value}) }} />
70+
</div>
71+
<div className="mt-3">
72+
<p>You can pass a variable for a specific org/project or use wildcard characters (*/*) to make it available in more projects.</p>
73+
</div>
74+
</div>
75+
<div className="flex justify-end mt-6">
76+
<button className="text-gray-900 border-white bg-white hover:border-gray-200" onClick={p.onClose}>Cancel</button>
77+
<button className={"ml-2 disabled:opacity-50"} onClick={save} >{isNew ? 'Add' : 'Update'} Variable</button>
78+
</div>
79+
</Modal>
80+
}
881

982
export default function EnvVars() {
10-
return <div>
11-
<SettingsPage title='Variables' subtitle='Configure environment variables for all workspaces.'>
12-
<h3>Environment Variables</h3>
13-
</SettingsPage>
14-
</div>;
83+
const [envVars, setEnvVars] = useState([] as UserEnvVarValue[]);
84+
const [currentEnvVar, setCurrentEnvVar] = useState({ name: '', value: '', repositoryPattern: '' } as UserEnvVarValue);
85+
const [isAddEnvVarModalVisible, setAddEnvVarModalVisible] = useState(false);
86+
const update = async () => {
87+
await getGitpodService().server.getEnvVars().then(r => setEnvVars(r));
88+
}
89+
90+
useEffect(() => {
91+
update()
92+
}, []);
93+
94+
95+
const add = () => {
96+
setCurrentEnvVar({ name: '', value: '', repositoryPattern: '' });
97+
setAddEnvVarModalVisible(true);
98+
}
99+
100+
const edit = (ev: UserEnvVarValue) => {
101+
setCurrentEnvVar(ev);
102+
setAddEnvVarModalVisible(true);
103+
}
104+
105+
const save = async (variable: UserEnvVarValue) => {
106+
await getGitpodService().server.setEnvVar(variable);
107+
await update();
108+
};
109+
110+
const deleteV = async (variable: UserEnvVarValue) => {
111+
await getGitpodService().server.deleteEnvVar(variable);
112+
await update();
113+
};
114+
115+
const validate = (variable: UserEnvVarValue) => {
116+
const name = variable.name;
117+
const pattern = variable.repositoryPattern;
118+
if (name.trim() === '') {
119+
return 'Name must not be empty.';
120+
}
121+
if (!/^[a-zA-Z0-9_]*$/.test(name)) {
122+
return 'Name must match /[a-zA-Z_]+[a-zA-Z0-9_]*/.';
123+
}
124+
if (variable.value.trim() === '') {
125+
return 'Value must not be empty.';
126+
}
127+
if (pattern.trim() === '') {
128+
return 'Scope must not be empty.';
129+
}
130+
const split = pattern.split('/');
131+
if (split.length < 2) {
132+
return "A scope must use the form 'organization/repo'.";
133+
}
134+
for (const name of split) {
135+
if (name !== '*') {
136+
if (!/^[a-zA-Z0-9_\-.\*]+$/.test(name)) {
137+
return 'Invalid scope segment. Only ASCII characters, numbers, -, _, . or * are allowed.';
138+
}
139+
}
140+
}
141+
return '';
142+
};
143+
144+
return <SettingsPage title='Variables' subtitle='Configure environment variables for all workspaces.'>
145+
{isAddEnvVarModalVisible ? <AddEnvVarModal
146+
save={save}
147+
envVar={currentEnvVar}
148+
validate={validate}
149+
onClose={() => setAddEnvVarModalVisible(false)} /> : null}
150+
{envVars.length === 0
151+
? <div className="bg-gray-100 rounded-xl w-full h-96">
152+
<div className="pt-28 flex flex-col items-center w-96 m-auto">
153+
<h3 className="text-center pb-3">No Environment Variables</h3>
154+
<div className="text-center pb-6 text-gray-500">In addition to user-specific environment variables you can also pass variables through a workspace creation URL. <a className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600" href="https://www.gitpod.io/docs/env-vars/">Learn more</a></div>
155+
<button onClick={add} className="font-medium">New Environment Variable</button>
156+
</div>
157+
</div>
158+
: <div>
159+
<div className="flex justify-end mb-2">
160+
<button onClick={add} className="ml-2 font-medium">New Environment Variable</button>
161+
</div>
162+
<div className="flex flex-col space-y-2">
163+
<div className="px-3 py-3 flex justify-between space-x-2 text-sm text-gray-400 border-t border-b border-gray-200">
164+
<div className="w-5/12">Name</div>
165+
<div className="w-5/12">Scope</div>
166+
<div className="w-2/12"></div>
167+
</div>
168+
</div>
169+
<div className="flex flex-col">
170+
{envVars.map(ev => {
171+
return <div className="rounded-xl whitespace-nowrap flex space-x-2 py-3 px-3 w-full justify-between hover:bg-gray-100 focus:bg-gitpod-kumquat-light group">
172+
<div className="w-5/12 m-auto">{ev.name}</div>
173+
<div className="w-5/12 m-auto text-sm text-gray-400">{ev.repositoryPattern}</div>
174+
<div className="w-2/12 flex justify-end">
175+
<div className="flex w-8 self-center hover:bg-gray-200 rounded-md cursor-pointer">
176+
<ContextMenu menuEntries={[
177+
{
178+
title: 'Edit',
179+
onClick: () => edit(ev),
180+
separator: true
181+
},
182+
{
183+
title: 'Delete',
184+
customFontStyle: 'text-red-600 hover:text-red-800',
185+
onClick: () => deleteV(ev)
186+
},
187+
]}>
188+
<img className="w-8 h-8 p-1" src={ThreeDots} alt="Actions" />
189+
</ContextMenu>
190+
</div>
191+
</div>
192+
</div>
193+
})}
194+
</div>
195+
</div>
196+
}
197+
</SettingsPage>;
15198
}

components/dashboard/src/settings/SettingsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface Props {
1717

1818
export function SettingsPage(p: Props) {
1919
const location = useLocation();
20-
return <div className="max-w-6xl">
20+
return <div className="w-full">
2121
<Header title={p.title} subtitle={p.subtitle}/>
2222
<div className='lg:px-28 px-10 flex pt-9'>
2323
<div>

0 commit comments

Comments
 (0)