ankka-console 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/auth/oidc.d.ts +64 -0
- package/dist/auth/oidc.js +199 -0
- package/dist/client/control-plane.d.ts +67 -0
- package/dist/client/control-plane.js +208 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/errors.js +59 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +3 -0
- package/dist/client/schemas.d.ts +365 -0
- package/dist/client/schemas.js +221 -0
- package/dist/context.d.ts +83 -0
- package/dist/context.js +156 -0
- package/dist/extensions/render.d.ts +20 -0
- package/dist/extensions/render.js +49 -0
- package/dist/extensions/types.d.ts +44 -0
- package/dist/extensions/types.js +30 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/middleware.d.ts +14 -0
- package/dist/middleware.js +48 -0
- package/dist/options.d.ts +45 -0
- package/dist/options.js +39 -0
- package/dist/routes/auth.callback.d.ts +11 -0
- package/dist/routes/auth.callback.js +43 -0
- package/dist/routes/auth.sign-in.d.ts +11 -0
- package/dist/routes/auth.sign-in.js +29 -0
- package/dist/routes/auth.sign-out.d.ts +19 -0
- package/dist/routes/auth.sign-out.js +37 -0
- package/dist/routes/front.d.ts +41 -0
- package/dist/routes/front.js +25 -0
- package/dist/routes/logs.d.ts +67 -0
- package/dist/routes/logs.js +72 -0
- package/dist/routes/members.d.ts +45 -0
- package/dist/routes/members.js +59 -0
- package/dist/routes/organization-new.d.ts +14 -0
- package/dist/routes/organization-new.js +30 -0
- package/dist/routes/organization.d.ts +43 -0
- package/dist/routes/organization.js +82 -0
- package/dist/routes/project-new.d.ts +27 -0
- package/dist/routes/project-new.js +27 -0
- package/dist/routes/project.d.ts +59 -0
- package/dist/routes/project.js +61 -0
- package/dist/routes/service-apply.d.ts +44 -0
- package/dist/routes/service-apply.js +65 -0
- package/dist/routes/service.d.ts +71 -0
- package/dist/routes/service.js +73 -0
- package/dist/routes/stream.project.d.ts +3 -0
- package/dist/routes/stream.project.js +25 -0
- package/dist/routes/stream.service.d.ts +9 -0
- package/dist/routes/stream.service.js +68 -0
- package/dist/routes/tokens.d.ts +47 -0
- package/dist/routes/tokens.js +50 -0
- package/dist/routes.d.ts +28 -0
- package/dist/routes.js +48 -0
- package/dist/runtime.d.ts +28 -0
- package/dist/runtime.js +77 -0
- package/dist/server/create-console-server.d.ts +33 -0
- package/dist/server/create-console-server.js +203 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +13 -0
- package/dist/server/transport.d.ts +25 -0
- package/dist/server/transport.js +97 -0
- package/dist/session/cookie-store.d.ts +51 -0
- package/dist/session/cookie-store.js +66 -0
- package/dist/session/cookies.d.ts +15 -0
- package/dist/session/cookies.js +33 -0
- package/dist/session/seal.d.ts +12 -0
- package/dist/session/seal.js +38 -0
- package/dist/session/token-cache.d.ts +21 -0
- package/dist/session/token-cache.js +47 -0
- package/dist/session/token-source.d.ts +41 -0
- package/dist/session/token-source.js +63 -0
- package/dist/stream/log-follow.d.ts +18 -0
- package/dist/stream/log-follow.js +40 -0
- package/dist/stream/registry.d.ts +6 -0
- package/dist/stream/registry.js +34 -0
- package/dist/stream/sse.d.ts +14 -0
- package/dist/stream/sse.js +87 -0
- package/dist/styles.css +481 -0
- package/dist/testing/fake-control-plane.d.ts +161 -0
- package/dist/testing/fake-control-plane.js +715 -0
- package/dist/testing/fake-issuer.d.ts +66 -0
- package/dist/testing/fake-issuer.js +303 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/scenarios.d.ts +6 -0
- package/dist/testing/scenarios.js +44 -0
- package/dist/ui/console.d.ts +60 -0
- package/dist/ui/console.js +73 -0
- package/dist/ui/errors.d.ts +6 -0
- package/dist/ui/errors.js +30 -0
- package/dist/ui/refused.d.ts +6 -0
- package/dist/ui/refused.js +12 -0
- package/dist/ui/status.d.ts +5 -0
- package/dist/ui/status.js +23 -0
- package/dist/ui/use-stream.d.ts +59 -0
- package/dist/ui/use-stream.js +67 -0
- package/package.json +79 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The front page: who you are, as the control plane sees you, and the organizations you can see —
|
|
3
|
+
* yours with your role in each, or every one of them for a platform administrator.
|
|
4
|
+
*/
|
|
5
|
+
import { type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
6
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
7
|
+
export declare const meta: MetaFunction;
|
|
8
|
+
export declare function loader({ context }: LoaderFunctionArgs): Promise<{
|
|
9
|
+
console: import("../context.ts").ConsolePageData;
|
|
10
|
+
whoami: {
|
|
11
|
+
subject: string;
|
|
12
|
+
name: string | undefined;
|
|
13
|
+
email: string | undefined;
|
|
14
|
+
emailVerified: boolean;
|
|
15
|
+
platformAdmin: boolean;
|
|
16
|
+
organizations: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
role: "member" | "owner";
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
organizations: {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
projects: number;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
28
|
+
quota: {
|
|
29
|
+
projects: number | undefined;
|
|
30
|
+
services: number | undefined;
|
|
31
|
+
instances: number | undefined;
|
|
32
|
+
} | undefined;
|
|
33
|
+
usage: {
|
|
34
|
+
projects: number;
|
|
35
|
+
services: number;
|
|
36
|
+
instances: number;
|
|
37
|
+
};
|
|
38
|
+
}[];
|
|
39
|
+
}>;
|
|
40
|
+
export default function Front(): import("react").JSX.Element;
|
|
41
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The front page: who you are, as the control plane sees you, and the organizations you can see —
|
|
4
|
+
* yours with your role in each, or every one of them for a platform administrator.
|
|
5
|
+
*/
|
|
6
|
+
import { useLoaderData } from "react-router";
|
|
7
|
+
import { guard, pageData, useConsoleContext } from "../context.js";
|
|
8
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
9
|
+
import { ConsoleLink, useConsole } from "../ui/console.js";
|
|
10
|
+
import { HostActions } from "../extensions/render.js";
|
|
11
|
+
export const meta = () => [{ title: "Organizations · ankka" }];
|
|
12
|
+
export async function loader({ context }) {
|
|
13
|
+
const ctx = useConsoleContext(context);
|
|
14
|
+
return guard(ctx, async () => {
|
|
15
|
+
const [whoami, organizations] = await Promise.all([ctx.client.whoami(), ctx.client.listOrganizations()]);
|
|
16
|
+
return { console: await pageData(ctx), whoami, organizations };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const roleWords = { owner: "Owner", member: "Member" };
|
|
20
|
+
export default function Front() {
|
|
21
|
+
const { whoami, organizations } = useLoaderData();
|
|
22
|
+
const { shows } = useConsole();
|
|
23
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx("h1", { children: "Organizations" }), _jsxs("p", { className: "ac-lede", children: ["Signed in as ", _jsx("strong", { children: whoami.name ?? whoami.email ?? whoami.subject }), whoami.email && whoami.name ? ` (${whoami.email})` : null, whoami.platformAdmin ? ", a platform administrator: you see every organization in this installation." : "."] }), organizations.length === 0 ? (_jsxs("p", { className: "ac-empty", children: ["You are not a member of any organization yet. Create one, or ask an owner to invite ", whoami.email ?? "you", "."] })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Organizations you can see" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Id" }), _jsx("th", { scope: "col", children: "Your role" }), _jsx("th", { scope: "col", className: "ac-num", children: "Projects" }), _jsx("th", { scope: "col", children: "State" })] }) }), _jsx("tbody", { children: organizations.map((o) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `organizations/${encodeURIComponent(o.id)}`, children: o.name }) }), _jsx("td", { children: o.id }), _jsx("td", { children: o.role ? roleWords[o.role] : "Administrator (not a member)" }), _jsx("td", { className: "ac-num", children: o.projects }), _jsx("td", { children: o.disabled ? "Disabled" : "Active" })] }, o.id))) })] }) })), _jsxs("div", { className: "ac-actions", children: [shows("organization.create") ? (_jsx(ConsoleLink, { to: "organizations/new", className: "ac-button", children: "Create an organization" })) : null, _jsx(HostActions, { operation: "organization.create" })] })] }));
|
|
24
|
+
}
|
|
25
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
2
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
3
|
+
export declare const meta: MetaFunction<typeof loader>;
|
|
4
|
+
export declare function loader({ request, params, context }: LoaderFunctionArgs): Promise<{
|
|
5
|
+
console: import("../context.ts").ConsolePageData;
|
|
6
|
+
service: {
|
|
7
|
+
name: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
lifecycle: string;
|
|
10
|
+
generation: number;
|
|
11
|
+
image: string;
|
|
12
|
+
readyInstances: number;
|
|
13
|
+
desiredInstances: number;
|
|
14
|
+
detail: string | undefined;
|
|
15
|
+
confirmed: boolean;
|
|
16
|
+
database: string | undefined;
|
|
17
|
+
hostname: string | undefined;
|
|
18
|
+
exposed: boolean;
|
|
19
|
+
suspended: boolean;
|
|
20
|
+
paused: boolean;
|
|
21
|
+
hosting: string;
|
|
22
|
+
protocol: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
project: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
organizationId: string;
|
|
28
|
+
registry: {
|
|
29
|
+
server: string;
|
|
30
|
+
username: string;
|
|
31
|
+
setAt: string | undefined;
|
|
32
|
+
setBy: string | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
};
|
|
35
|
+
organization: {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
projects: number;
|
|
39
|
+
disabled: boolean;
|
|
40
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
41
|
+
quota: {
|
|
42
|
+
projects: number | undefined;
|
|
43
|
+
services: number | undefined;
|
|
44
|
+
instances: number | undefined;
|
|
45
|
+
} | undefined;
|
|
46
|
+
usage: {
|
|
47
|
+
projects: number;
|
|
48
|
+
services: number;
|
|
49
|
+
instances: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
instances: {
|
|
53
|
+
instance: string;
|
|
54
|
+
lines: string[];
|
|
55
|
+
error?: string;
|
|
56
|
+
}[];
|
|
57
|
+
none: string | undefined;
|
|
58
|
+
query: {
|
|
59
|
+
instance: string | undefined;
|
|
60
|
+
previous: boolean;
|
|
61
|
+
tail: number;
|
|
62
|
+
since: number | undefined;
|
|
63
|
+
};
|
|
64
|
+
known: string[];
|
|
65
|
+
}>;
|
|
66
|
+
export default function Logs(): import("react").JSX.Element;
|
|
67
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A service's logs, with the CLI's four choices: one instance or all, the container before the last
|
|
4
|
+
* restart, how many lines, how far back. With scripts running the page follows new lines as they are
|
|
5
|
+
* written, until the person pauses it; without, it shows what was there when the page was asked for.
|
|
6
|
+
*/
|
|
7
|
+
import { useMemo, useState } from "react";
|
|
8
|
+
import { Form, useLoaderData } from "react-router";
|
|
9
|
+
import { guard, pageData, useConsoleContext } from "../context.js";
|
|
10
|
+
import { ControlPlaneError } from "../client/errors.js";
|
|
11
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
12
|
+
import { Breadcrumbs, useConsole } from "../ui/console.js";
|
|
13
|
+
import { useServiceStream } from "../ui/use-stream.js";
|
|
14
|
+
import { lines } from "../stream/log-follow.js";
|
|
15
|
+
export const meta = ({ loaderData }) => [{ title: `Logs of ${loaderData?.service.name ?? "a service"} · ankka` }];
|
|
16
|
+
export async function loader({ request, params, context }) {
|
|
17
|
+
const ctx = useConsoleContext(context);
|
|
18
|
+
const { projectId, name } = params;
|
|
19
|
+
const q = new URL(request.url).searchParams;
|
|
20
|
+
const query = {
|
|
21
|
+
instance: q.get("instance") || undefined,
|
|
22
|
+
previous: q.get("previous") === "true",
|
|
23
|
+
tail: q.get("tail") ? Number(q.get("tail")) : 200,
|
|
24
|
+
since: q.get("since") ? Number(q.get("since")) : undefined,
|
|
25
|
+
};
|
|
26
|
+
return guard(ctx, async () => {
|
|
27
|
+
const [service, project, page] = await Promise.all([ctx.client.getService(projectId, name), ctx.client.getProject(projectId), pageData(ctx)]);
|
|
28
|
+
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
29
|
+
let instances = [];
|
|
30
|
+
let none;
|
|
31
|
+
try {
|
|
32
|
+
const logs = await ctx.client.logs(projectId, name, query);
|
|
33
|
+
instances = logs.instances.map((i) => ({ instance: i.instance, lines: lines(i.output), error: i.error }));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e instanceof ControlPlaneError && e.status === 404)
|
|
37
|
+
none = e.reason;
|
|
38
|
+
else
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
// Every instance the service has had output from, for the choice; the current read may be of one.
|
|
42
|
+
const known = query.instance ? [query.instance, ...instances.map((i) => i.instance).filter((i) => i !== query.instance)] : instances.map((i) => i.instance);
|
|
43
|
+
return { console: page, service, project, organization, instances, none, query, known };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export default function Logs() {
|
|
47
|
+
const { service, project: p, organization: o, instances, none, query, known } = useLoaderData();
|
|
48
|
+
const { mount } = useConsole();
|
|
49
|
+
const [follow, setFollow] = useState(true);
|
|
50
|
+
const initial = useMemo(() => Object.fromEntries(instances.map((i) => [i.instance, { lines: i.lines, error: i.error }])), [instances]);
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (query.instance)
|
|
53
|
+
params.set("instance", query.instance);
|
|
54
|
+
if (query.previous)
|
|
55
|
+
params.set("previous", "true");
|
|
56
|
+
params.set("tail", String(query.tail));
|
|
57
|
+
const { byInstance, state } = useServiceStream(p.id, service.name, service, {
|
|
58
|
+
follow: follow && !query.previous,
|
|
59
|
+
query: params.toString(),
|
|
60
|
+
tail: query.tail,
|
|
61
|
+
initial,
|
|
62
|
+
});
|
|
63
|
+
const shown = Object.entries(byInstance);
|
|
64
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx(Breadcrumbs, { trail: [
|
|
65
|
+
{ label: "Organizations", to: "" },
|
|
66
|
+
{ label: o.name, to: `organizations/${encodeURIComponent(o.id)}` },
|
|
67
|
+
{ label: p.name, to: `projects/${encodeURIComponent(p.id)}` },
|
|
68
|
+
{ label: service.name, to: `projects/${encodeURIComponent(p.id)}/services/${encodeURIComponent(service.name)}` },
|
|
69
|
+
{ label: "Logs" },
|
|
70
|
+
] }), _jsxs("h1", { children: ["Logs of ", service.name] }), _jsxs(Form, { method: "get", className: "ac-inline", "aria-label": "Which logs", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "instance", children: "Instance" }), _jsxs("select", { id: "instance", name: "instance", defaultValue: query.instance ?? "", children: [_jsx("option", { value: "", children: "Every instance" }), known.map((i) => (_jsx("option", { value: i, children: i }, i)))] })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "tail", children: "Last lines" }), _jsx("input", { id: "tail", name: "tail", type: "number", min: 1, max: 5000, defaultValue: query.tail })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "since", children: "Last seconds" }), _jsx("input", { id: "since", name: "since", type: "number", min: 1, defaultValue: query.since, placeholder: "Any" })] }), _jsx("div", { className: "ac-field", children: _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "previous", value: "true", defaultChecked: query.previous }), " The container before the last restart"] }) }), _jsx("button", { type: "submit", className: "ac-button", children: "Show" })] }), query.previous ? null : (_jsxs("p", { className: "ac-actions", children: [follow ? _jsx("span", { className: state === "live" ? "ac-live" : undefined, children: state === "live" ? "Following new lines" : "Connecting to follow new lines" }) : _jsx("span", { children: "Paused" }), _jsx("button", { type: "button", className: "ac-button ac-button-quiet", onClick: () => setFollow((f) => !f), hidden: state === "off" && !follow ? false : undefined, children: follow ? "Pause following" : "Follow new lines" }), _jsx("noscript", { children: _jsx("a", { href: `${mount}projects/${encodeURIComponent(p.id)}/services/${encodeURIComponent(service.name)}/logs?${params}`, children: "Refresh" }) })] })), none ? _jsxs("p", { className: "ac-empty", children: [none, ". Logs exist only while an instance runs."] }) : null, !none && shown.length === 0 ? _jsx("p", { className: "ac-empty", children: "No output yet." }) : null, shown.map(([instance, log]) => (_jsxs("section", { "aria-labelledby": `log-${instance}`, children: [_jsx("h2", { id: `log-${instance}`, children: instance }), log.error ? _jsx("p", { className: "ac-refusal", children: log.error }) : null, _jsx("pre", { className: "ac-log", tabIndex: 0, "data-instance": instance, children: log.lines.join("\n") })] }, instance)))] }));
|
|
71
|
+
}
|
|
72
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization's members and the invitations waiting to be claimed. Everyone in it sees the
|
|
3
|
+
* list; only owners see the controls. A machine — a deploy token — is a member like anyone else,
|
|
4
|
+
* and is shown as the machine it is.
|
|
5
|
+
*/
|
|
6
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
7
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
8
|
+
export declare const meta: MetaFunction;
|
|
9
|
+
export declare function loader({ params, context }: LoaderFunctionArgs): Promise<{
|
|
10
|
+
members: {
|
|
11
|
+
subject: string;
|
|
12
|
+
role: "member" | "owner";
|
|
13
|
+
email: string | undefined;
|
|
14
|
+
display: string | undefined;
|
|
15
|
+
since: string | undefined;
|
|
16
|
+
addedBy: string | undefined;
|
|
17
|
+
}[];
|
|
18
|
+
invitations: {
|
|
19
|
+
email: string;
|
|
20
|
+
role: "member" | "owner";
|
|
21
|
+
invitedAt: string | undefined;
|
|
22
|
+
invitedBy: string | undefined;
|
|
23
|
+
}[];
|
|
24
|
+
console: import("../context.ts").ConsolePageData;
|
|
25
|
+
organization: {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
projects: number;
|
|
29
|
+
disabled: boolean;
|
|
30
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
31
|
+
quota: {
|
|
32
|
+
projects: number | undefined;
|
|
33
|
+
services: number | undefined;
|
|
34
|
+
instances: number | undefined;
|
|
35
|
+
} | undefined;
|
|
36
|
+
usage: {
|
|
37
|
+
projects: number;
|
|
38
|
+
services: number;
|
|
39
|
+
instances: number;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
44
|
+
export default function Members(): import("react").JSX.Element;
|
|
45
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An organization's members and the invitations waiting to be claimed. Everyone in it sees the
|
|
4
|
+
* list; only owners see the controls. A machine — a deploy token — is a member like anyone else,
|
|
5
|
+
* and is shown as the machine it is.
|
|
6
|
+
*/
|
|
7
|
+
import { redirect, useLoaderData } from "react-router";
|
|
8
|
+
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
9
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
+
import { Breadcrumbs, ConsoleForm, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
11
|
+
import { Refused, useRefusal } from "../ui/refused.js";
|
|
12
|
+
import { HostActions } from "../extensions/render.js";
|
|
13
|
+
export const meta = () => [{ title: "Members · ankka" }];
|
|
14
|
+
export async function loader({ params, context }) {
|
|
15
|
+
const ctx = useConsoleContext(context);
|
|
16
|
+
const id = params.organizationId;
|
|
17
|
+
return guard(ctx, async () => {
|
|
18
|
+
const [organization, members, page] = await Promise.all([ctx.client.getOrganization(id), ctx.client.members(id), pageData(ctx)]);
|
|
19
|
+
return { console: page, organization, ...members };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const role = (form) => (text(form, "role") === "owner" ? "owner" : "member");
|
|
23
|
+
export async function action({ request, params, context }) {
|
|
24
|
+
const ctx = useConsoleContext(context);
|
|
25
|
+
const id = params.organizationId;
|
|
26
|
+
const form = await request.formData();
|
|
27
|
+
const intent = text(form, "intent");
|
|
28
|
+
const self = ctx.href(`organizations/${encodeURIComponent(id)}/members`);
|
|
29
|
+
return act(ctx, intent, form, async () => {
|
|
30
|
+
switch (intent) {
|
|
31
|
+
case "invite":
|
|
32
|
+
await ctx.client.invite(id, text(form, "email"), role(form));
|
|
33
|
+
return redirect(self);
|
|
34
|
+
case "role":
|
|
35
|
+
await ctx.client.changeRole(id, text(form, "subject"), role(form));
|
|
36
|
+
return redirect(self);
|
|
37
|
+
case "remove":
|
|
38
|
+
await ctx.client.removeMember(id, text(form, "subject"));
|
|
39
|
+
return redirect(self);
|
|
40
|
+
case "withdraw":
|
|
41
|
+
await ctx.client.withdrawInvitation(id, text(form, "email"));
|
|
42
|
+
return redirect(self);
|
|
43
|
+
default:
|
|
44
|
+
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const roleWord = (r) => (r === "owner" ? "Owner" : "Member");
|
|
49
|
+
export default function Members() {
|
|
50
|
+
const { organization: o, members, invitations, console: page } = useLoaderData();
|
|
51
|
+
const { shows } = useConsole();
|
|
52
|
+
const owner = o.role === "owner" || (page.principal?.platformAdmin ?? false);
|
|
53
|
+
const inviteRefusal = useRefusal("invite");
|
|
54
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx(Breadcrumbs, { trail: [{ label: "Organizations", to: "" }, { label: o.name, to: `organizations/${encodeURIComponent(o.id)}` }, { label: "Members" }] }), _jsxs("h1", { children: ["Members of ", o.name] }), _jsx("p", { className: "ac-lede", children: "Owners manage members and deploy tokens; every member can deploy." }), _jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Members" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Who" }), _jsx("th", { scope: "col", children: "Role" }), _jsx("th", { scope: "col", children: "Since" }), _jsx("th", { scope: "col", children: "Added by" }), owner ? _jsx("th", { scope: "col", children: "Change" }) : null] }) }), _jsx("tbody", { children: members.map((m) => {
|
|
55
|
+
const machine = m.subject.startsWith("token:");
|
|
56
|
+
return (_jsxs("tr", { "data-member": m.subject, children: [_jsxs("td", { children: [machine ? "Deploy token " : null, _jsx("strong", { children: m.display ?? m.email ?? m.subject }), m.email && m.display ? _jsx("div", { className: "ac-hint", children: m.email }) : null, _jsx("div", { className: "ac-hint", children: m.subject })] }), _jsx("td", { children: roleWord(m.role) }), _jsx("td", { children: when(m.since) }), _jsx("td", { children: m.addedBy ?? "—" }), owner ? (_jsx("td", { children: machine ? (_jsx("span", { className: "ac-hint", children: "Revoke it on the deploy tokens page" })) : (_jsxs("div", { className: "ac-actions", children: [shows("member.role") ? (_jsxs(ConsoleForm, { intent: "role", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx("input", { type: "hidden", name: "role", value: m.role === "owner" ? "member" : "owner" }), _jsx(Submit, { intent: "role", children: m.role === "owner" ? "Make member" : "Make owner" })] })) : null, shows("member.remove") ? (_jsxs(ConsoleForm, { intent: "remove", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx(Submit, { intent: "remove", danger: true, children: "Remove" })] })) : null] })) })) : null] }, m.subject));
|
|
57
|
+
}) })] }) }), _jsx(Refused, { intent: "role" }), _jsx(Refused, { intent: "remove" }), _jsx("h2", { children: "Invitations" }), invitations.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No invitations are waiting." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Invitations waiting to be claimed" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Email" }), _jsx("th", { scope: "col", children: "Role" }), _jsx("th", { scope: "col", children: "Invited" }), owner ? _jsx("th", { scope: "col", children: "Change" }) : null] }) }), _jsx("tbody", { children: invitations.map((i) => (_jsxs("tr", { "data-invitation": i.email, children: [_jsx("td", { children: i.email }), _jsx("td", { children: roleWord(i.role) }), _jsxs("td", { children: [when(i.invitedAt), i.invitedBy ? ` by ${i.invitedBy}` : ""] }), owner ? (_jsx("td", { children: shows("invitation.withdraw") ? (_jsxs(ConsoleForm, { intent: "withdraw", children: [_jsx("input", { type: "hidden", name: "email", value: i.email }), _jsx(Submit, { intent: "withdraw", children: "Withdraw" })] })) : null })) : null] }, i.email))) })] }) })), _jsx(Refused, { intent: "withdraw" }), owner && shows("member.invite") ? (_jsxs(_Fragment, { children: [_jsx("h2", { children: "Invite someone" }), _jsx("p", { children: "They become a member the first time they sign in with this email address, once the identity provider has verified it." }), _jsxs(ConsoleForm, { intent: "invite", className: "ac-inline", children: [_jsx(Field, { label: "Email", name: "email", type: "email", required: true, autoComplete: "off", defaultValue: inviteRefusal?.values.email }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "invite-role", children: "Role" }), _jsxs("select", { id: "invite-role", name: "role", defaultValue: inviteRefusal?.values.role ?? "member", children: [_jsx("option", { value: "member", children: "Member" }), _jsx("option", { value: "owner", children: "Owner" })] })] }), _jsx(Submit, { intent: "invite", children: "Invite" })] }), _jsx(Refused, { intent: "invite" }), _jsx("div", { className: "ac-actions", children: _jsx(HostActions, { operation: "member.invite", entity: o }) })] })) : null] }));
|
|
58
|
+
}
|
|
59
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an organization. Its id is permanent and never reused; the person creating it becomes its
|
|
3
|
+
* first owner. On success the page goes to the organization itself, read by id, because the listing
|
|
4
|
+
* is a projection and may not show it yet.
|
|
5
|
+
*/
|
|
6
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
7
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
8
|
+
export declare const meta: MetaFunction;
|
|
9
|
+
export declare function loader({ context }: LoaderFunctionArgs): Promise<{
|
|
10
|
+
console: import("../context.ts").ConsolePageData;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function action({ request, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
13
|
+
export default function NewOrganization(): import("react").JSX.Element;
|
|
14
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Creates an organization. Its id is permanent and never reused; the person creating it becomes its
|
|
4
|
+
* first owner. On success the page goes to the organization itself, read by id, because the listing
|
|
5
|
+
* is a projection and may not show it yet.
|
|
6
|
+
*/
|
|
7
|
+
import { redirect } from "react-router";
|
|
8
|
+
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
9
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
+
import { Breadcrumbs, ConsoleForm, Field, Submit } from "../ui/console.js";
|
|
11
|
+
import { Refused, useRefusal } from "../ui/refused.js";
|
|
12
|
+
export const meta = () => [{ title: "Create an organization · ankka" }];
|
|
13
|
+
export async function loader({ context }) {
|
|
14
|
+
const ctx = useConsoleContext(context);
|
|
15
|
+
return guard(ctx, async () => ({ console: await pageData(ctx) }));
|
|
16
|
+
}
|
|
17
|
+
export async function action({ request, context }) {
|
|
18
|
+
const ctx = useConsoleContext(context);
|
|
19
|
+
const form = await request.formData();
|
|
20
|
+
const id = text(form, "id");
|
|
21
|
+
return act(ctx, "create", form, async () => {
|
|
22
|
+
await ctx.client.createOrganization(id, { name: text(form, "name") });
|
|
23
|
+
return redirect(ctx.href(`organizations/${encodeURIComponent(id)}`));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export default function NewOrganization() {
|
|
27
|
+
const refusal = useRefusal("create");
|
|
28
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx(Breadcrumbs, { trail: [{ label: "Organizations", to: "" }, { label: "New organization" }] }), _jsx("h1", { children: "Create an organization" }), _jsx("p", { className: "ac-lede", children: "You become its first owner. Invite others once it exists." }), _jsxs(ConsoleForm, { to: "organizations/new", intent: "create", className: "ac-form", children: [_jsx(Field, { label: "Id", name: "id", required: true, pattern: "[a-z]([\\-a-z0-9]{0,61}[a-z0-9])?", hint: "Lowercase letters, digits and hyphens, starting with a letter. It cannot be changed, and once deleted it cannot be used again.", defaultValue: refusal?.values.id, autoComplete: "off" }), _jsx(Field, { label: "Name", name: "name", required: true, hint: "What people see. You can rename it later.", defaultValue: refusal?.values.name }), _jsx(Refused, { intent: "create" }), _jsx("div", { children: _jsx(Submit, { intent: "create", children: "Create organization" }) })] })] }));
|
|
29
|
+
}
|
|
30
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization: its projects, and what its owners and the platform administrator may do to it.
|
|
3
|
+
*/
|
|
4
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
5
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
6
|
+
export declare const meta: MetaFunction<typeof loader>;
|
|
7
|
+
export declare function loader({ params, context }: LoaderFunctionArgs): Promise<{
|
|
8
|
+
console: import("../context.ts").ConsolePageData;
|
|
9
|
+
organization: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
projects: number;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
15
|
+
quota: {
|
|
16
|
+
projects: number | undefined;
|
|
17
|
+
services: number | undefined;
|
|
18
|
+
instances: number | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
usage: {
|
|
21
|
+
projects: number;
|
|
22
|
+
services: number;
|
|
23
|
+
instances: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
projects: {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
organizationId: string;
|
|
30
|
+
services: number;
|
|
31
|
+
registry: {
|
|
32
|
+
server: string;
|
|
33
|
+
username: string;
|
|
34
|
+
setAt: string | undefined;
|
|
35
|
+
setBy: string | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
}[];
|
|
38
|
+
ownerless: boolean;
|
|
39
|
+
panels: import("../extensions/render.tsx").LoadedPanels;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
42
|
+
export default function Organization(): import("react").JSX.Element;
|
|
43
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An organization: its projects, and what its owners and the platform administrator may do to it.
|
|
4
|
+
*/
|
|
5
|
+
import { redirect, useLoaderData } from "react-router";
|
|
6
|
+
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
7
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
8
|
+
import { Breadcrumbs, ConsoleForm, ConsoleLink, Field, Submit, useConsole } from "../ui/console.js";
|
|
9
|
+
import { Refused, useRefusal } from "../ui/refused.js";
|
|
10
|
+
import { HostActions, loadPanels, Panels } from "../extensions/render.js";
|
|
11
|
+
export const meta = ({ loaderData }) => [{ title: `${loaderData?.organization.name ?? "Organization"} · ankka` }];
|
|
12
|
+
export async function loader({ params, context }) {
|
|
13
|
+
const ctx = useConsoleContext(context);
|
|
14
|
+
const id = params.organizationId;
|
|
15
|
+
return guard(ctx, async () => {
|
|
16
|
+
const [organization, projects, page] = await Promise.all([ctx.client.getOrganization(id), ctx.client.listProjects(), pageData(ctx)]);
|
|
17
|
+
// An organization whose owners have all left can be given one by an administrator.
|
|
18
|
+
const ownerless = page.principal?.platformAdmin ? !(await ctx.client.members(id)).members.some((m) => m.role === "owner") : false;
|
|
19
|
+
return {
|
|
20
|
+
console: page,
|
|
21
|
+
organization,
|
|
22
|
+
projects: projects.filter((p) => p.organizationId === id),
|
|
23
|
+
ownerless,
|
|
24
|
+
panels: await loadPanels(ctx, "organization", organization),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const quotaValue = (form, name) => {
|
|
29
|
+
const v = text(form, name);
|
|
30
|
+
return v === "" ? undefined : Number(v);
|
|
31
|
+
};
|
|
32
|
+
export async function action({ request, params, context }) {
|
|
33
|
+
const ctx = useConsoleContext(context);
|
|
34
|
+
const id = params.organizationId;
|
|
35
|
+
const form = await request.formData();
|
|
36
|
+
const intent = text(form, "intent");
|
|
37
|
+
const self = ctx.href(`organizations/${encodeURIComponent(id)}`);
|
|
38
|
+
return act(ctx, intent, form, async () => {
|
|
39
|
+
switch (intent) {
|
|
40
|
+
case "rename":
|
|
41
|
+
await ctx.client.renameOrganization(id, text(form, "name"));
|
|
42
|
+
return redirect(self);
|
|
43
|
+
case "delete":
|
|
44
|
+
await ctx.client.deleteOrganization(id);
|
|
45
|
+
return redirect(ctx.href(""));
|
|
46
|
+
case "disable":
|
|
47
|
+
await ctx.client.disableOrganization(id);
|
|
48
|
+
return redirect(self);
|
|
49
|
+
case "enable":
|
|
50
|
+
await ctx.client.enableOrganization(id);
|
|
51
|
+
return redirect(self);
|
|
52
|
+
case "quota-set": {
|
|
53
|
+
const quota = { projects: quotaValue(form, "projects"), services: quotaValue(form, "services"), instances: quotaValue(form, "instances") };
|
|
54
|
+
await ctx.client.setQuota(id, quota);
|
|
55
|
+
return redirect(self);
|
|
56
|
+
}
|
|
57
|
+
case "quota-clear":
|
|
58
|
+
await ctx.client.clearQuota(id);
|
|
59
|
+
return redirect(self);
|
|
60
|
+
case "repair":
|
|
61
|
+
await ctx.client.repair(id, text(form, "subject"), "owner");
|
|
62
|
+
return redirect(self);
|
|
63
|
+
default:
|
|
64
|
+
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const limit = (n, what) => (n === undefined ? `any number of ${what}` : `${n} ${what}`);
|
|
69
|
+
export default function Organization() {
|
|
70
|
+
const { organization: o, projects, ownerless, panels, console: page } = useLoaderData();
|
|
71
|
+
const { shows } = useConsole();
|
|
72
|
+
const admin = page.principal?.platformAdmin ?? false;
|
|
73
|
+
const owner = o.role === "owner" || admin;
|
|
74
|
+
const renameRefusal = useRefusal("rename");
|
|
75
|
+
const deleteRefusal = useRefusal("delete");
|
|
76
|
+
const adminRefused = [useRefusal("disable"), useRefusal("enable"), useRefusal("quota-set"), useRefusal("quota-clear"), useRefusal("repair")].some(Boolean);
|
|
77
|
+
const path = `organizations/${encodeURIComponent(o.id)}`;
|
|
78
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx(Breadcrumbs, { trail: [{ label: "Organizations", to: "" }, { label: o.name }] }), _jsx("h1", { children: o.name }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Id" }), _jsx("dd", { children: o.id }), _jsx("dt", { children: "Your role" }), _jsx("dd", { children: o.role === "owner" ? "Owner" : o.role === "member" ? "Member" : "Administrator (not a member)" }), _jsx("dt", { children: "State" }), _jsx("dd", { children: o.disabled ? "Disabled: its services are suspended and every change is refused" : "Active" }), _jsx("dt", { children: "Holds" }), _jsxs("dd", { children: [o.usage.projects, " projects, ", o.usage.services, " services, ", o.usage.instances, " instances"] }), o.quota ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Quota" }), _jsxs("dd", { children: [limit(o.quota.projects, "projects"), ", ", limit(o.quota.services, "services"), ", ", limit(o.quota.instances, "instances")] })] })) : null] }), _jsxs("nav", { "aria-label": "Organization", className: "ac-actions", children: [_jsx(ConsoleLink, { to: `${path}/members`, children: "Members" }), o.role === "owner" || admin ? _jsx(ConsoleLink, { to: `${path}/tokens`, children: "Deploy tokens" }) : null] }), _jsx("h2", { children: "Projects" }), projects.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No projects yet. A project groups services and gives them a namespace of their own." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsxs("caption", { className: "ac-visually-hidden", children: ["Projects in ", o.name] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Id" }), _jsx("th", { scope: "col", className: "ac-num", children: "Services" }), _jsx("th", { scope: "col", children: "Registry" })] }) }), _jsx("tbody", { children: projects.map((p) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `projects/${encodeURIComponent(p.id)}`, children: p.name }) }), _jsx("td", { children: p.id }), _jsx("td", { className: "ac-num", children: p.services }), _jsx("td", { children: p.registry ? p.registry.server : "Public images only" })] }, p.id))) })] }) })), _jsxs("div", { className: "ac-actions", children: [shows("project.create") ? (_jsx(ConsoleLink, { to: `${path}/projects/new`, className: "ac-button", children: "Create a project" })) : null, _jsx(HostActions, { operation: "project.create", entity: o })] }), _jsx(Panels, { kind: "organization", entity: o, loaded: panels }), owner && (shows("organization.rename") || shows("organization.delete")) ? (_jsxs("details", { className: "ac-more", open: renameRefusal !== undefined || deleteRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Rename or delete" }), shows("organization.rename") ? (_jsxs(ConsoleForm, { intent: "rename", className: "ac-inline", children: [_jsx(Field, { label: "New name", name: "name", required: true, defaultValue: renameRefusal?.values.name ?? o.name }), _jsx(Submit, { intent: "rename", children: "Rename" })] })) : null, _jsx(Refused, { intent: "rename" }), shows("organization.delete") ? (_jsxs("div", { className: "ac-danger-zone", children: [_jsxs("p", { children: ["Deleting ", o.name, " is permanent, and its id can never be used again. It must have no projects."] }), _jsx(ConsoleForm, { intent: "delete", children: _jsx(Submit, { intent: "delete", danger: true, children: "Delete organization" }) }), _jsx(Refused, { intent: "delete" })] })) : null, _jsx(HostActions, { operation: "organization.delete", entity: o })] })) : null, admin ? (_jsxs("details", { className: "ac-more", open: adminRefused || undefined, children: [_jsx("summary", { children: "Platform administration" }), _jsx("div", { className: "ac-actions", children: o.disabled
|
|
79
|
+
? shows("organization.enable") && (_jsx(ConsoleForm, { intent: "enable", children: _jsx(Submit, { intent: "enable", children: "Enable organization" }) }))
|
|
80
|
+
: shows("organization.disable") && (_jsx(ConsoleForm, { intent: "disable", children: _jsx(Submit, { intent: "disable", danger: true, children: "Disable organization" }) })) }), _jsx(Refused, { intent: "disable" }), _jsx(Refused, { intent: "enable" }), shows("organization.quota.set") ? (_jsxs(ConsoleForm, { intent: "quota-set", className: "ac-inline", "aria-label": "Quota", children: [_jsx(Field, { label: "Projects", name: "projects", type: "number", min: 0, defaultValue: o.quota?.projects, hint: "Empty for no limit" }), _jsx(Field, { label: "Services", name: "services", type: "number", min: 0, defaultValue: o.quota?.services }), _jsx(Field, { label: "Instances", name: "instances", type: "number", min: 0, defaultValue: o.quota?.instances }), _jsx(Submit, { intent: "quota-set", children: "Set quota" })] })) : null, _jsx(Refused, { intent: "quota-set" }), o.quota && shows("organization.quota.clear") ? (_jsx(ConsoleForm, { intent: "quota-clear", children: _jsx(Submit, { intent: "quota-clear", children: "Clear quota" }) })) : null, ownerless && shows("member.repair") ? (_jsxs(ConsoleForm, { intent: "repair", className: "ac-inline", children: [_jsx(Field, { label: "Subject to make owner", name: "subject", required: true, hint: "This organization has no owner. Name the subject of the person to make one." }), _jsx(Submit, { intent: "repair", children: "Add owner" })] })) : null, _jsx(Refused, { intent: "repair" })] })) : null] }));
|
|
81
|
+
}
|
|
82
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Creates a project in an organization, then goes to it, read by id. */
|
|
2
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
3
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
4
|
+
export declare const meta: MetaFunction;
|
|
5
|
+
export declare function loader({ params, context }: LoaderFunctionArgs): Promise<{
|
|
6
|
+
console: import("../context.ts").ConsolePageData;
|
|
7
|
+
organization: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
projects: number;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
13
|
+
quota: {
|
|
14
|
+
projects: number | undefined;
|
|
15
|
+
services: number | undefined;
|
|
16
|
+
instances: number | undefined;
|
|
17
|
+
} | undefined;
|
|
18
|
+
usage: {
|
|
19
|
+
projects: number;
|
|
20
|
+
services: number;
|
|
21
|
+
instances: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
26
|
+
export default function NewProject(): import("react").JSX.Element;
|
|
27
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Creates a project in an organization, then goes to it, read by id. */
|
|
3
|
+
import { redirect, useLoaderData } from "react-router";
|
|
4
|
+
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
5
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
6
|
+
import { Breadcrumbs, ConsoleForm, Field, Submit } from "../ui/console.js";
|
|
7
|
+
import { Refused, useRefusal } from "../ui/refused.js";
|
|
8
|
+
export const meta = () => [{ title: "Create a project · ankka" }];
|
|
9
|
+
export async function loader({ params, context }) {
|
|
10
|
+
const ctx = useConsoleContext(context);
|
|
11
|
+
return guard(ctx, async () => ({ console: await pageData(ctx), organization: await ctx.client.getOrganization(params.organizationId) }));
|
|
12
|
+
}
|
|
13
|
+
export async function action({ request, params, context }) {
|
|
14
|
+
const ctx = useConsoleContext(context);
|
|
15
|
+
const form = await request.formData();
|
|
16
|
+
const id = text(form, "id");
|
|
17
|
+
return act(ctx, "create", form, async () => {
|
|
18
|
+
await ctx.client.createProject(id, text(form, "name"), params.organizationId);
|
|
19
|
+
return redirect(ctx.href(`projects/${encodeURIComponent(id)}`));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export default function NewProject() {
|
|
23
|
+
const { organization: o } = useLoaderData();
|
|
24
|
+
const refusal = useRefusal("create");
|
|
25
|
+
return (_jsxs("section", { className: "ac-page", children: [_jsx(Breadcrumbs, { trail: [{ label: "Organizations", to: "" }, { label: o.name, to: `organizations/${encodeURIComponent(o.id)}` }, { label: "New project" }] }), _jsxs("h1", { children: ["Create a project in ", o.name] }), _jsx("p", { className: "ac-lede", children: "A project gets a namespace of its own; each of its services gets its own database." }), _jsxs(ConsoleForm, { intent: "create", className: "ac-form", children: [_jsx(Field, { label: "Id", name: "id", required: true, pattern: "[a-z]([\\-a-z0-9]{0,61}[a-z0-9])?", hint: "Lowercase letters, digits and hyphens, starting with a letter. Part of every exposed service's address, and never reusable once deleted.", defaultValue: refusal?.values.id, autoComplete: "off" }), _jsx(Field, { label: "Name", name: "name", required: true, defaultValue: refusal?.values.name }), _jsx(Refused, { intent: "create" }), _jsx("div", { children: _jsx(Submit, { intent: "create", children: "Create project" }) })] })] }));
|
|
26
|
+
}
|
|
27
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project: its services, kept current while the page is open, and its registry credential.
|
|
3
|
+
*/
|
|
4
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
5
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
6
|
+
export declare const meta: MetaFunction<typeof loader>;
|
|
7
|
+
export declare function loader({ params, context }: LoaderFunctionArgs): Promise<{
|
|
8
|
+
console: import("../context.ts").ConsolePageData;
|
|
9
|
+
project: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
organizationId: string;
|
|
13
|
+
registry: {
|
|
14
|
+
server: string;
|
|
15
|
+
username: string;
|
|
16
|
+
setAt: string | undefined;
|
|
17
|
+
setBy: string | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
};
|
|
20
|
+
organization: {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
projects: number;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
26
|
+
quota: {
|
|
27
|
+
projects: number | undefined;
|
|
28
|
+
services: number | undefined;
|
|
29
|
+
instances: number | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
usage: {
|
|
32
|
+
projects: number;
|
|
33
|
+
services: number;
|
|
34
|
+
instances: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
services: {
|
|
38
|
+
name: string;
|
|
39
|
+
projectId: string;
|
|
40
|
+
lifecycle: string;
|
|
41
|
+
generation: number;
|
|
42
|
+
image: string;
|
|
43
|
+
readyInstances: number;
|
|
44
|
+
desiredInstances: number;
|
|
45
|
+
detail: string | undefined;
|
|
46
|
+
confirmed: boolean;
|
|
47
|
+
database: string | undefined;
|
|
48
|
+
hostname: string | undefined;
|
|
49
|
+
exposed: boolean;
|
|
50
|
+
suspended: boolean;
|
|
51
|
+
paused: boolean;
|
|
52
|
+
hosting: string;
|
|
53
|
+
protocol: string | undefined;
|
|
54
|
+
}[];
|
|
55
|
+
panels: import("../extensions/render.tsx").LoadedPanels;
|
|
56
|
+
}>;
|
|
57
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
58
|
+
export default function Project(): import("react").JSX.Element;
|
|
59
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|