canUserUpdate build and build owner config

This commit is contained in:
mbecker20
2022-04-08 15:11:22 -07:00
parent 43342fb9bf
commit e0385cc47c
18 changed files with 364 additions and 77 deletions

View File

@@ -1,3 +1,4 @@
import { BUILD_OWNER_UPDATE } from "@monitor/util";
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
@@ -22,6 +23,66 @@ const builds = fp((app: FastifyInstance, _: {}, done: () => void) => {
res.send(state);
}
);
app.post(
"/api/build/:id/:owner",
{ onRequest: [app.auth, app.userEnabled] },
async (req, res) => {
// adds an owner to a build
const { id, owner } = req.params as { id: string; owner: string };
const sender = (await app.users.findById(req.user.id))!;
if (sender.permissions! < 1) {
res.status(403);
res.send("inadequate permissions");
return;
}
const user = await app.users.findOne({ username: owner });
if (!user || user.permissions! < 1) {
res.status(400);
res.send("invalid user");
return;
}
const build = await app.builds.findById(id);
if (!build) {
res.status(400);
res.send("build not found");
return;
}
if (sender.permissions! < 2 && !build.owners.includes(sender.username)) {
res.status(403);
res.send("inadequate permissions");
return;
}
await app.builds.updateById(id, { $push: { owners: owner } });
app.broadcast(BUILD_OWNER_UPDATE, { buildID: id });
res.send("owner added");
}
);
app.delete(
"/api/build/:id/:owner",
{ onRequest: [app.auth, app.userEnabled] },
async (req, res) => {
// removes owner from deployment
const { id, owner } = req.params as { id: string; owner: string };
const sender = (await app.users.findById(req.user.id))!;
if (sender.permissions! < 2) {
res.status(403);
res.send("inadequate permissions");
return;
}
const build = await app.builds.findById(id);
if (!build) {
res.status(400);
res.send("build not found");
return;
}
await app.builds.updateById(id, { $pull: { owners: owner } });
app.broadcast(BUILD_OWNER_UPDATE, { buildID: id });
res.send("owner removed");
}
);
done();
});

View File

@@ -0,0 +1,92 @@
import { User } from "@monitor/types";
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { pushNotification } from "../../..";
import { useUser } from "../../../state/UserProvider";
import {
addOwnerToBuild,
getUsers,
removeOwnerFromBuild,
} from "../../../util/query";
import ConfirmButton from "../../util/ConfirmButton";
import Input from "../../util/Input";
import Flex from "../../util/layout/Flex";
import Grid from "../../util/layout/Grid";
import Menu from "../../util/menu/Menu";
import { useConfig } from "./Provider";
const Owners: Component<{}> = (p) => {
const { build } = useConfig();
const { permissions, username } = useUser();
const [userSearch, setUserSearch] = createSignal("");
const [users, setUsers] = createSignal<User[]>([]);
createEffect(() => {
if (userSearch().length > 0) {
getUsers(userSearch(), true).then((users) => {
setUsers(users.filter((user) => !build.owners.includes(user.username)));
});
} else {
setUsers([]);
}
});
return (
<Grid class="config-item shadow">
<Menu
show={userSearch() ? true : false}
close={() => setUserSearch("")}
target={
<Input
placeholder="add user"
value={userSearch()}
onEdit={setUserSearch}
style={{ width: "12rem" }}
/>
}
content={
<>
<For each={users()}>
{(user) => (
<ConfirmButton
color="grey"
style={{ width: "100%", "justify-content": "flex-start" }}
onConfirm={async () => {
await addOwnerToBuild(build._id!, user.username);
pushNotification("good", "owner added to deployment");
setUserSearch("");
}}
confirmText="add user"
>
{user.username}
</ConfirmButton>
)}
</For>
<Show when={users().length === 0}>no matching users</Show>
</>
}
style={{ width: "12rem" }}
/>
<For each={build.owners}>
{(owner) => (
<Flex alignItems="center" justifyContent="space-between">
<div class="big-text">
{owner}
{owner === username() && " ( you )"}
</div>
<Show when={permissions() > 1}>
<ConfirmButton
color="red"
onConfirm={async () => {
await removeOwnerFromBuild(build._id!, owner);
pushNotification("good", "user removed from collaborators");
}}
>
remove
</ConfirmButton>
</Show>
</Flex>
)}
</For>
</Grid>
);
};
export default Owners;

View File

@@ -1,23 +1,36 @@
import { Build, Update } from "@monitor/types";
import { Component, createContext, createEffect, onCleanup, useContext } from "solid-js";
import {
Component,
createContext,
createEffect,
onCleanup,
useContext,
} from "solid-js";
import { createStore, DeepReadonly, SetStoreFunction } from "solid-js/store";
import { ADD_UPDATE, UPDATE_BUILD } from "../../../state/actions";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { getBuild } from "../../../util/query";
type ConfigBuild = Build & { loaded: boolean; updated: boolean; saving: boolean };
type ConfigBuild = Build & {
loaded: boolean;
updated: boolean;
saving: boolean;
};
type State = {
build: DeepReadonly<ConfigBuild>;
setBuild: SetStoreFunction<ConfigBuild>;
reset: () => void;
save: () => void;
userCanUpdate: () => boolean;
};
const context = createContext<State>();
export const ConfigProvider: Component<{}> = (p) => {
const { ws, selected, builds } = useAppState();
const { permissions, username } = useUser();
const [build, set] = createStore({
...builds.get(selected.id())!,
loaded: false,
@@ -31,7 +44,7 @@ export const ConfigProvider: Component<{}> = (p) => {
};
const load = () => {
console.log("load server");
console.log("load build");
getBuild(selected.id()).then((build) => {
set({
...build,
@@ -44,7 +57,7 @@ export const ConfigProvider: Component<{}> = (p) => {
githubAccount: build.githubAccount,
loaded: true,
updated: false,
saving: false
saving: false,
});
});
};
@@ -55,23 +68,31 @@ export const ConfigProvider: Component<{}> = (p) => {
ws.send(UPDATE_BUILD, { build });
};
const unsub = ws.subscribe(
[ADD_UPDATE],
({ update }: { update: Update }) => {
if (update.buildID === selected.id()) {
if ([UPDATE_BUILD].includes(update.operation)) {
load();
}
}
}
);
onCleanup(unsub);
const unsub = ws.subscribe([ADD_UPDATE], ({ update }: { update: Update }) => {
if (update.buildID === selected.id()) {
if ([UPDATE_BUILD].includes(update.operation)) {
load();
}
}
});
onCleanup(unsub);
const userCanUpdate = () => {
if (permissions() > 1) {
return true;
} else if (permissions() > 0 && build.owners.includes(username()!)) {
return true;
} else {
return false;
}
};
const state = {
build,
setBuild,
reset: load,
save,
userCanUpdate,
};
return <context.Provider value={state}>{p.children}</context.Provider>;
};

View File

@@ -1,16 +1,28 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import Tabs from "../../util/tabs/Tabs";
import BuildConfig from "./build-config/BuildConfig";
import GitConfig from "./git-config/GitConfig";
import Owners from "./Owners";
import { ConfigProvider } from "./Provider";
const BuildTabs: Component<{}> = (p) => {
const { builds, selected } = useAppState();
const { username, permissions } = useUser();
const build = () => builds.get(selected.id())!;
const userCanUpdate = () => {
if (permissions() > 1) {
return true;
} else if (permissions() > 0 && build().owners.includes(username()!)) {
return true;
} else {
return false;
}
};
return (
<Show when={build()}>
<ConfigProvider build={build()}>
<ConfigProvider>
<Tabs
containerClass="card tabs shadow"
tabs={[
@@ -20,8 +32,12 @@ const BuildTabs: Component<{}> = (p) => {
},
{
title: "build",
element: <BuildConfig />
}
element: <BuildConfig />,
},
userCanUpdate() && {
title: "collaborators",
element: <Owners />,
},
]}
localStorageKey="build-tab"
/>

View File

@@ -9,7 +9,7 @@ import { useConfig } from "../Provider";
import Loading from "../../../util/loading/Loading";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save } = useConfig();
const { build, reset, save, userCanUpdate } = useConfig();
return (
<Show when={build.loaded}>
<Grid class="config">
@@ -17,7 +17,7 @@ const BuildConfig: Component<{}> = (p) => {
<Docker />
<CliBuild />
</Grid>
<Show when={build.updated}>
<Show when={userCanUpdate() && build.updated}>
<Show
when={!build.saving}
fallback={

View File

@@ -5,25 +5,33 @@ import Grid from "../../../util/layout/Grid";
import { useConfig } from "../Provider";
const CliBuild: Component<{}> = (p) => {
const { build, setBuild } = useConfig();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>cli build</h1>
<div style={{ opacity: 0.7 }}>build with a custom command</div>
<Flex justifyContent="space-between" alignItems="center">
<div>build path</div>
{/* <div style={{ opacity: 0.7 }}>build with a custom command</div> */}
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>build path: </h2>
<Input
placeholder="from root of repo"
value={build.cliBuild?.path || ""}
value={build.cliBuild?.path || (userCanUpdate() ? "" : "/")}
onEdit={(path) => setBuild("cliBuild", { path })}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex justifyContent="space-between" alignItems="center">
<div>command</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>command: </h2>
<Input
placeholder="ie. yarn build"
value={build.cliBuild?.command || ""}
onEdit={(command) => setBuild("cliBuild", { command })}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>

View File

@@ -10,31 +10,45 @@ import { useConfig } from "../Provider";
const Docker: Component<{}> = (p) => {
const { dockerAccounts } = useAppState();
const { build, setBuild } = useConfig();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>docker build</h1> {/* checkbox here? */}
<Flex justifyContent="space-between" alignItems="center">
<div>build path</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>build path: </h2>
<Input
placeholder="from root of repo"
value={build.dockerBuildArgs?.buildPath || ""}
onEdit={(buildPath) => setBuild("dockerBuildArgs", { buildPath })}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex justifyContent="space-between" alignItems="center">
<div>dockerfile path</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>dockerfile path: </h2>
<Input
placeholder="from root of build path"
value={build.dockerBuildArgs?.dockerfilePath || ""}
value={
build.dockerBuildArgs?.dockerfilePath ||
(userCanUpdate() ? "" : "./dockerfile")
}
onEdit={(dockerfilePath) =>
setBuild("dockerBuildArgs", { dockerfilePath })
}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={dockerAccounts() && dockerAccounts()!.length > 0}>
<Flex justifyContent="space-between" alignItems="center">
<div>account</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>account: </h2>
<Selector
targetClass="blue"
selected={build.dockerAccount || "none"}
@@ -46,6 +60,7 @@ const Docker: Component<{}> = (p) => {
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>

View File

@@ -1,5 +1,4 @@
import { Component, Show } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import { Component, createEffect, Show } from "solid-js";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../util/layout/Flex";
@@ -9,29 +8,41 @@ import Selector from "../../../util/menu/Selector";
const Git: Component<{}> = (p) => {
const { githubAccounts } = useAppState();
const { build, setBuild } = useConfig();
const { build, setBuild, userCanUpdate } = useConfig();
createEffect(() => console.log(build.branch));
return (
<Grid class="config-item shadow">
<h1>github config</h1>
<Flex justifyContent="space-between" alignItems="center">
<div>repo</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex justifyContent="space-between" alignItems="center">
<div>branch</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || ""}
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={githubAccounts() && githubAccounts()!.length > 0}>
<Flex justifyContent="space-between" alignItems="center">
<div>github account</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.githubAccount || "none"}
@@ -43,6 +54,7 @@ const Git: Component<{}> = (p) => {
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>

View File

@@ -1,5 +1,4 @@
import { Component, Show } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import ConfirmButton from "../../../util/ConfirmButton";
import Icon from "../../../util/Icon";
import Flex from "../../../util/layout/Flex";
@@ -10,7 +9,7 @@ import Git from "./Git";
import OnClone from "./OnClone";
const GitConfig: Component<{}> = (p) => {
const { build, reset, save } = useConfig();
const { build, reset, save, userCanUpdate } = useConfig();
return (
<Show when={build.loaded}>
<Grid class="config">
@@ -18,7 +17,7 @@ const GitConfig: Component<{}> = (p) => {
<Git />
<OnClone />
</Grid>
<Show when={build.updated}>
<Show when={userCanUpdate() && build.updated}>
<Show
when={!build.saving}
fallback={

View File

@@ -7,12 +7,15 @@ import s from "../../build.module.css";
import Flex from "../../../util/layout/Flex";
const OnClone: Component = () => {
const { build, setBuild } = useConfig();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>on clone</h1>
<Flex alignItems="center" justifyContent="space-between">
path:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={build.onClone?.path || ""}
@@ -27,10 +30,14 @@ const OnClone: Component = () => {
}
setBuild("onClone", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex alignItems="center" justifyContent="space-between">
command:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>command:</h2>
<Input
placeholder="command"
value={build.onClone?.command || ""}
@@ -45,6 +52,7 @@ const OnClone: Component = () => {
}
setBuild("onClone", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>

View File

@@ -44,7 +44,7 @@ const Config: Component<{}> = (p) => {
</Grid>
),
},
{
(userCanUpdate() || deployment.repo ? true : false) && {
title: "repo mount",
element: (
<Grid class="config-items scroller">
@@ -58,7 +58,7 @@ const Config: Component<{}> = (p) => {
userCanUpdate() && {
title: "collaborators",
element: (
<Grid class="config-items scroller">
<Grid class="config-items scroller" style={{ height: "100%" }}>
<Owners />
</Grid>
),

View File

@@ -8,29 +8,40 @@ import { useConfig } from "../Provider";
const Git: Component<{}> = (p) => {
const { githubAccounts } = useAppState();
const { deployment, setDeployment } = useConfig();
const { deployment, setDeployment, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>github config</h1>
<Flex justifyContent="space-between" alignItems="center">
<div>repo</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={deployment.repo || ""}
onEdit={(value) => setDeployment("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex justifyContent="space-between" alignItems="center">
<div>branch</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={deployment.branch || ""}
onEdit={(value) => setDeployment("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={githubAccounts() && githubAccounts()!.length > 0}>
<Flex justifyContent="space-between" alignItems="center">
<div>github account</div>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={deployment.githubAccount || "none"}
@@ -42,6 +53,7 @@ const Git: Component<{}> = (p) => {
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>

View File

@@ -5,15 +5,18 @@ import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
export const OnClone: Component<{}> = (p) => {
const { deployment, setDeployment } = useConfig();
const { deployment, setDeployment, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>on clone</h1>
<Flex alignItems="center" justifyContent="space-between">
path:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={deployment.onClone?.path || ""}
value={deployment.onClone?.path || (userCanUpdate() ? "" : "/")}
onEdit={(path) => {
if (
path.length === 0 &&
@@ -25,10 +28,14 @@ export const OnClone: Component<{}> = (p) => {
}
setDeployment("onClone", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex alignItems="center" justifyContent="space-between">
command:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>command:</h2>
<Input
placeholder="command"
value={deployment.onClone?.command || ""}
@@ -43,6 +50,7 @@ export const OnClone: Component<{}> = (p) => {
}
setDeployment("onClone", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
@@ -50,12 +58,15 @@ export const OnClone: Component<{}> = (p) => {
};
export const OnPull: Component<{}> = (p) => {
const { deployment, setDeployment } = useConfig();
const { deployment, setDeployment, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>on pull</h1>
<Flex alignItems="center" justifyContent="space-between">
path:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={deployment.onPull?.path || ""}
@@ -70,10 +81,14 @@ export const OnPull: Component<{}> = (p) => {
}
setDeployment("onPull", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex alignItems="center" justifyContent="space-between">
command:
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<h2>command:</h2>
<Input
placeholder="command"
value={deployment.onPull?.command || ""}
@@ -88,6 +103,7 @@ export const OnPull: Component<{}> = (p) => {
}
setDeployment("onPull", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>

View File

@@ -5,16 +5,20 @@ import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
const RepoMount: Component<{}> = (p) => {
const { deployment, setDeployment } = useConfig();
const { deployment, setDeployment, userCanUpdate } = useConfig();
return (
<Grid class="config-item shadow">
<h1>mount</h1>
<Flex alignItems="center" justifyContent="space-between">
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
>
<Input
placeholder="repo folder to mount"
value={deployment.repoMount || ""}
style={{ width: "40%" }}
onEdit={(value) => setDeployment("repoMount", value)}
disabled={!userCanUpdate()}
/>
{" : "}
<Input
@@ -22,6 +26,7 @@ const RepoMount: Component<{}> = (p) => {
value={deployment.containerMount || ""}
style={{ width: "40%" }}
onEdit={(value) => setDeployment("containerMount", value)}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>

View File

@@ -25,9 +25,9 @@ const Account: Component<{ close: () => void }> = (p) => {
manage users
</button>
</Show>
<ConfirmButton onConfirm={logout} color="red">
<button onClick={logout} class="red">
log out
</ConfirmButton>
</button>
</Grid>
);
};

View File

@@ -53,6 +53,7 @@
}
.config-items {
height: fit-content;
max-height: 65vh;
padding: 0rem 0.5rem;
}

View File

@@ -23,6 +23,13 @@ h1 {
margin: 0;
}
h2 {
font-size: 1.1rem;
font-weight: 450;
user-select: none;
margin: 0;
}
button {
color: inherit;
padding: 0.5rem;

View File

@@ -39,6 +39,20 @@ export async function getBuildActionState(buildID: string) {
);
}
export async function addOwnerToBuild(
buildID: string,
username: string
) {
return await client.post(`/api/build/${buildID}/${username}`);
}
export async function removeOwnerFromBuild(
buildID: string,
username: string
) {
return await client.delete(`/api/build/${buildID}/${username}`);
}
export async function getDeployments(query?: { serverID?: string }) {
return await client.get<Deployments>(
"/api/deployments" + generateQuery(query)