@skillcdn/console 0.1.14 → 0.1.15
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 +5 -4
- package/SKILLCDN.md +2 -0
- package/dist/agent-name.d.ts +3 -0
- package/dist/agent-name.d.ts.map +1 -0
- package/dist/agent-name.js +9 -0
- package/dist/agent-name.js.map +1 -0
- package/dist/api.d.ts +6 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +215 -22
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +14 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +59 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +39 -29
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/connect.d.ts +4 -1
- package/dist/components/connect.d.ts.map +1 -1
- package/dist/components/connect.js +7 -3
- package/dist/components/connect.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +2 -2
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts +5 -0
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -2
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +10 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/people.d.ts +24 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +35 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +6 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/rules.d.ts +40 -0
- package/dist/components/rules.d.ts.map +1 -0
- package/dist/components/rules.js +46 -0
- package/dist/components/rules.js.map +1 -0
- package/dist/components/runs.d.ts +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +5 -1
- package/dist/components/skills.js.map +1 -1
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +2 -9
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +13 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +5 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +1 -1
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +219 -13
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +197 -43
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +83 -9
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +81 -11
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +81 -11
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +30 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +50 -10
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -4
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -2
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +28 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +102 -0
- package/dist/rules.js.map +1 -0
- package/dist/schemas.d.ts +652 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -6
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +21 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +13 -9
- package/docs/specs/rest.md +32 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +1 -1
- package/skills/deploying-a-console/SKILL.md +5 -5
- package/skills/deploying-a-console/assets/secrets.example.json +2 -1
- package/skills/deploying-a-console/references/access.md +10 -6
- package/skills/deploying-a-console/references/resources.md +10 -7
- package/skills/working-the-board/SKILL.md +10 -9
- package/skills/working-the-board/references/project-rules.md +36 -0
- package/src/agent-name.ts +9 -0
- package/src/api.ts +37 -0
- package/src/cli/cli.ts +253 -24
- package/src/cli/format.ts +74 -10
- package/src/cli/help.ts +30 -10
- package/src/cli/main.ts +5 -0
- package/src/client.ts +47 -1
- package/src/components/calendar.tsx +63 -50
- package/src/components/connect.tsx +12 -6
- package/src/components/decision-list.tsx +2 -1
- package/src/components/documents.tsx +7 -3
- package/src/components/event-feed.tsx +16 -3
- package/src/components/people.tsx +244 -35
- package/src/components/projects.tsx +31 -8
- package/src/components/rules.tsx +167 -0
- package/src/components/runs.tsx +4 -2
- package/src/components/skills.tsx +22 -10
- package/src/components/tokens.tsx +2 -14
- package/src/components/ui.tsx +25 -2
- package/src/components/work.tsx +34 -28
- package/src/console.tsx +523 -45
- package/src/data.ts +52 -2
- package/src/i18n/en.ts +91 -12
- package/src/i18n/ko.ts +87 -12
- package/src/index.ts +16 -1
- package/src/limits.ts +4 -0
- package/src/router.ts +67 -12
- package/src/routes.ts +23 -3
- package/src/rules.ts +109 -0
- package/src/schemas.ts +121 -4
- package/src/styles/console.css +219 -13
- package/src/vocabulary.ts +30 -0
|
@@ -1,17 +1,46 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FormEvent, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
hostOfLogin,
|
|
4
|
+
isLoginAt,
|
|
5
|
+
type PersonRole,
|
|
6
|
+
type PersonStatus,
|
|
7
|
+
type ProviderKey,
|
|
8
|
+
type RestListing,
|
|
9
|
+
type RestListingInput,
|
|
10
|
+
type RestOrganization,
|
|
11
|
+
type RestPerson,
|
|
12
|
+
type RestProvider,
|
|
13
|
+
} from "../api.js";
|
|
2
14
|
import { useWords } from "../i18n/index.js";
|
|
15
|
+
import { MAX_LOGIN_LENGTH } from "../limits.js";
|
|
3
16
|
import { PERSON_ROLES } from "../vocabulary.js";
|
|
4
|
-
import { PersonChip, personName, RoleBadge } from "./ui.js";
|
|
17
|
+
import { Badge, Button, PersonChip, personName, RoleBadge, Time } from "./ui.js";
|
|
5
18
|
|
|
6
|
-
// The people of the board, with what each may do
|
|
7
|
-
//
|
|
19
|
+
// The people of the board, with what each may do and whether they are in (ADR-0022): those in,
|
|
20
|
+
// the logins let in that nobody has signed in with yet, and those deactivated. An administrator
|
|
21
|
+
// changes roles here, takes people out and back in, and takes a listing away; a member reads.
|
|
22
|
+
// Takes its data as props and nothing from the network.
|
|
8
23
|
|
|
9
24
|
export interface PeopleListProps {
|
|
10
25
|
readonly people: readonly RestPerson[];
|
|
26
|
+
/** The logins let in that nobody has signed in with yet. */
|
|
27
|
+
readonly listed?: readonly RestListing[] | undefined;
|
|
28
|
+
/** The organization at the git host that says who is in, where the deployment names one. */
|
|
29
|
+
readonly organization?: RestOrganization | null | undefined;
|
|
11
30
|
/** Whoever is looking, so that their own row says so. */
|
|
12
31
|
readonly me?: RestPerson | undefined;
|
|
13
32
|
/** Called with the role an administrator chose. Left out, roles are read here, not changed. */
|
|
14
33
|
readonly onChangeRole?: ((person: RestPerson, role: PersonRole) => void) | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Called when an administrator takes a person out, or back in. Left out, standings are read
|
|
36
|
+
* here, not changed. Never offered for a person the organization let in, who comes and goes
|
|
37
|
+
* with it.
|
|
38
|
+
*/
|
|
39
|
+
readonly onChangeStatus?: ((person: RestPerson, status: PersonStatus) => void) | undefined;
|
|
40
|
+
/** Called when an administrator takes a listing away. Left out, listings are read here. */
|
|
41
|
+
readonly onUnlist?: ((listing: RestListing) => void) | undefined;
|
|
42
|
+
/** Where a person's work is seen; given to an administrator, or an owner of a project. */
|
|
43
|
+
readonly workHref?: ((person: RestPerson) => string) | undefined;
|
|
15
44
|
/** Where a person's agents are seen; given to an administrator. */
|
|
16
45
|
readonly agentsHref?: ((person: RestPerson) => string) | undefined;
|
|
17
46
|
readonly busy?: boolean | undefined;
|
|
@@ -19,41 +48,221 @@ export interface PeopleListProps {
|
|
|
19
48
|
|
|
20
49
|
export function PeopleList(props: PeopleListProps) {
|
|
21
50
|
const words = useWords();
|
|
51
|
+
const people = words.people;
|
|
52
|
+
const active = props.people.filter((person) => person.status === "active");
|
|
53
|
+
const deactivated = props.people.filter((person) => person.status === "deactivated");
|
|
54
|
+
const listed = props.listed ?? [];
|
|
55
|
+
const mayChange = (person: RestPerson): boolean =>
|
|
56
|
+
props.onChangeStatus !== undefined &&
|
|
57
|
+
person.source !== "organization" &&
|
|
58
|
+
props.me?.id !== person.id;
|
|
59
|
+
const organization = props.organization ?? undefined;
|
|
22
60
|
return (
|
|
23
|
-
<
|
|
24
|
-
{
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{props.me?.id === person.id && <span> {words.common.you}</span>}
|
|
30
|
-
</span>
|
|
31
|
-
{props.onChangeRole === undefined ? (
|
|
32
|
-
<RoleBadge role={person.role} />
|
|
61
|
+
<div className="sc-people-page">
|
|
62
|
+
{organization !== undefined && (
|
|
63
|
+
<p className="sc-people-organization sc-muted">
|
|
64
|
+
{people.organization(organization.name)}{" "}
|
|
65
|
+
{organization.syncedAt === null ? (
|
|
66
|
+
people.organizationNotRead
|
|
33
67
|
) : (
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
68
|
+
<>
|
|
69
|
+
{people.organizationRead} <Time iso={organization.syncedAt} />
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
<ul className="sc-people" aria-label={people.list}>
|
|
75
|
+
{active.map((person) => (
|
|
76
|
+
<li key={person.id} className="sc-person-row">
|
|
77
|
+
<PersonChip person={person} size="md" />
|
|
78
|
+
<span className="sc-person-name sc-muted">
|
|
79
|
+
{person.name === null ? "" : person.login}
|
|
80
|
+
{props.me?.id === person.id && <span> {words.common.you}</span>}
|
|
81
|
+
<span className="sc-person-note"> {people.source[person.source]}</span>
|
|
82
|
+
</span>
|
|
83
|
+
{props.onChangeRole === undefined ? (
|
|
84
|
+
<RoleBadge role={person.role} />
|
|
85
|
+
) : (
|
|
86
|
+
<label className="sc-field sc-field-inline">
|
|
87
|
+
<span className="sc-visually-hidden">{people.roleOf(personName(person))}</span>
|
|
88
|
+
<select
|
|
89
|
+
className="sc-input"
|
|
90
|
+
value={person.role}
|
|
91
|
+
disabled={props.busy === true}
|
|
92
|
+
onChange={(event) =>
|
|
93
|
+
props.onChangeRole?.(person, event.target.value as PersonRole)
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
{PERSON_ROLES.map((role) => (
|
|
97
|
+
<option key={role} value={role}>
|
|
98
|
+
{words.vocabulary.role[role]}
|
|
99
|
+
</option>
|
|
100
|
+
))}
|
|
101
|
+
</select>
|
|
102
|
+
</label>
|
|
103
|
+
)}
|
|
104
|
+
{props.workHref !== undefined && (
|
|
105
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.workHref(person)}>
|
|
106
|
+
{people.work}
|
|
107
|
+
</a>
|
|
108
|
+
)}
|
|
109
|
+
{props.agentsHref !== undefined && (
|
|
110
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.agentsHref(person)}>
|
|
111
|
+
{people.agents}
|
|
112
|
+
</a>
|
|
113
|
+
)}
|
|
114
|
+
{mayChange(person) && (
|
|
115
|
+
<Button
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="sm"
|
|
39
118
|
disabled={props.busy === true}
|
|
40
|
-
|
|
119
|
+
onClick={() => props.onChangeStatus?.(person, "deactivated")}
|
|
41
120
|
>
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
121
|
+
{people.deactivate}
|
|
122
|
+
</Button>
|
|
123
|
+
)}
|
|
124
|
+
</li>
|
|
125
|
+
))}
|
|
126
|
+
</ul>
|
|
127
|
+
{listed.length > 0 && (
|
|
128
|
+
<section className="sc-people-section" aria-label={people.notSignedIn}>
|
|
129
|
+
<h2 className="sc-section-title">{people.notSignedIn}</h2>
|
|
130
|
+
<ul className="sc-people">
|
|
131
|
+
{listed.map((listing) => (
|
|
132
|
+
<li key={listing.id} className="sc-person-row">
|
|
133
|
+
<span className="sc-person-login">{listing.login}</span>
|
|
134
|
+
<span className="sc-person-name sc-muted">
|
|
135
|
+
{people.at[listing.host]}
|
|
136
|
+
<span className="sc-person-note">
|
|
137
|
+
{" "}
|
|
138
|
+
<Time iso={listing.createdAt} />
|
|
139
|
+
</span>
|
|
140
|
+
</span>
|
|
141
|
+
{props.onUnlist !== undefined && (
|
|
142
|
+
<Button
|
|
143
|
+
variant="ghost"
|
|
144
|
+
size="sm"
|
|
145
|
+
disabled={props.busy === true}
|
|
146
|
+
onClick={() => props.onUnlist?.(listing)}
|
|
147
|
+
>
|
|
148
|
+
{words.common.remove}
|
|
149
|
+
</Button>
|
|
150
|
+
)}
|
|
151
|
+
</li>
|
|
152
|
+
))}
|
|
153
|
+
</ul>
|
|
154
|
+
</section>
|
|
155
|
+
)}
|
|
156
|
+
{deactivated.length > 0 && (
|
|
157
|
+
<section className="sc-people-section" aria-label={people.deactivated}>
|
|
158
|
+
<h2 className="sc-section-title">{people.deactivated}</h2>
|
|
159
|
+
<ul className="sc-people">
|
|
160
|
+
{deactivated.map((person) => (
|
|
161
|
+
<li key={person.id} className="sc-person-row sc-person-row-out">
|
|
162
|
+
<PersonChip person={person} size="md" />
|
|
163
|
+
<span className="sc-person-name sc-muted">
|
|
164
|
+
{person.name === null ? "" : person.login}
|
|
165
|
+
<span className="sc-person-note">
|
|
166
|
+
{" "}
|
|
167
|
+
{person.source === "organization"
|
|
168
|
+
? people.leftOrganization
|
|
169
|
+
: people.source[person.source]}
|
|
170
|
+
{person.deactivatedAt !== null && (
|
|
171
|
+
<>
|
|
172
|
+
{" "}
|
|
173
|
+
<Time iso={person.deactivatedAt} />
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
<Badge tone="neutral">{words.vocabulary.status.deactivated}</Badge>
|
|
179
|
+
{mayChange(person) && (
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="sm"
|
|
183
|
+
disabled={props.busy === true}
|
|
184
|
+
onClick={() => props.onChangeStatus?.(person, "active")}
|
|
185
|
+
>
|
|
186
|
+
{people.reactivate}
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
</li>
|
|
190
|
+
))}
|
|
191
|
+
</ul>
|
|
192
|
+
</section>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ListingFormProps {
|
|
199
|
+
/** The providers people sign in through here: what a listing may name. */
|
|
200
|
+
readonly providers: readonly RestProvider[];
|
|
201
|
+
readonly busy?: boolean | undefined;
|
|
202
|
+
readonly onSubmit: (input: RestListingInput) => void;
|
|
203
|
+
readonly onCancel?: (() => void) | undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Where an administrator lets someone in by login: whoever signs in with that account is in. */
|
|
207
|
+
export function ListingForm(props: ListingFormProps) {
|
|
208
|
+
const words = useWords();
|
|
209
|
+
const form = words.people.form;
|
|
210
|
+
const [login, setLogin] = useState("");
|
|
211
|
+
const [chosen, setChosen] = useState<ProviderKey | "">("");
|
|
212
|
+
const only = props.providers.length === 1 ? props.providers[0]?.key : undefined;
|
|
213
|
+
/** The provider named, else the one the login's shape says, else the only one. */
|
|
214
|
+
const host: ProviderKey | undefined =
|
|
215
|
+
chosen !== "" ? chosen : only !== undefined ? only : hostOfLogin(login.trim());
|
|
216
|
+
const offered = props.providers.some((provider) => provider.key === host);
|
|
217
|
+
const valid = host !== undefined && offered && isLoginAt(host, login.trim());
|
|
218
|
+
const submit = (event: FormEvent) => {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
if (valid && host !== undefined) {
|
|
221
|
+
props.onSubmit({ host, login: login.trim() });
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
return (
|
|
225
|
+
<form className="sc-form sc-listing-form" aria-label={words.people.letInAria} onSubmit={submit}>
|
|
226
|
+
<div className="sc-field-row">
|
|
227
|
+
<label className="sc-field">
|
|
228
|
+
<span className="sc-field-label">{form.login}</span>
|
|
229
|
+
<input
|
|
230
|
+
className="sc-input"
|
|
231
|
+
value={login}
|
|
232
|
+
maxLength={MAX_LOGIN_LENGTH}
|
|
233
|
+
autoComplete="off"
|
|
234
|
+
onChange={(event) => setLogin(event.target.value)}
|
|
235
|
+
/>
|
|
236
|
+
</label>
|
|
237
|
+
{props.providers.length > 1 && (
|
|
238
|
+
<label className="sc-field">
|
|
239
|
+
<span className="sc-field-label">{form.provider}</span>
|
|
240
|
+
<select
|
|
241
|
+
className="sc-input"
|
|
242
|
+
value={chosen}
|
|
243
|
+
onChange={(event) => setChosen(event.target.value as ProviderKey | "")}
|
|
244
|
+
>
|
|
245
|
+
<option value="">{form.byShape}</option>
|
|
246
|
+
{props.providers.map((provider) => (
|
|
247
|
+
<option key={provider.key} value={provider.key}>
|
|
248
|
+
{provider.label}
|
|
249
|
+
</option>
|
|
250
|
+
))}
|
|
251
|
+
</select>
|
|
252
|
+
</label>
|
|
253
|
+
)}
|
|
254
|
+
<div className="sc-form-actions">
|
|
255
|
+
<Button type="submit" variant="primary" disabled={props.busy === true || !valid}>
|
|
256
|
+
{form.submit}
|
|
257
|
+
</Button>
|
|
258
|
+
{props.onCancel !== undefined && (
|
|
259
|
+
<Button type="button" variant="ghost" onClick={props.onCancel}>
|
|
260
|
+
{words.common.cancel}
|
|
261
|
+
</Button>
|
|
54
262
|
)}
|
|
55
|
-
</
|
|
56
|
-
|
|
57
|
-
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
<p className="sc-field-hint">{form.hint}</p>
|
|
266
|
+
</form>
|
|
58
267
|
);
|
|
59
268
|
}
|
|
@@ -119,7 +119,11 @@ export function ProjectList(props: ProjectListProps) {
|
|
|
119
119
|
</a>
|
|
120
120
|
<code className="sc-project-key">{project.key}</code>
|
|
121
121
|
<ProjectRoleBadge role={project.role} />
|
|
122
|
-
{project.visibility === "private"
|
|
122
|
+
{project.visibility === "private" ? (
|
|
123
|
+
<Badge tone="neutral">{words.private}</Badge>
|
|
124
|
+
) : (
|
|
125
|
+
<Badge tone="warning">{words.public}</Badge>
|
|
126
|
+
)}
|
|
123
127
|
</div>
|
|
124
128
|
{project.description.length > 0 && (
|
|
125
129
|
<p className="sc-project-description">{project.description}</p>
|
|
@@ -146,6 +150,10 @@ export interface ProjectFormProps {
|
|
|
146
150
|
*/
|
|
147
151
|
readonly onSubmit: (input: RestProjectInput | RestProjectPatch, icon?: File | null) => void;
|
|
148
152
|
readonly onCancel?: (() => void) | undefined;
|
|
153
|
+
/** The origin of the SkillCDN deployment the skills are read through, shown before the address typed. */
|
|
154
|
+
readonly skillsSource?: string | undefined;
|
|
155
|
+
/** The organization's address, which a project without one of its own reads; `null` for none. */
|
|
156
|
+
readonly organizationAddress?: string | null | undefined;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/** A key as a person types a name: lowercase letters, digits and hyphens, from what they typed. */
|
|
@@ -272,13 +280,21 @@ export function ProjectForm(props: ProjectFormProps) {
|
|
|
272
280
|
</label>
|
|
273
281
|
<label className="sc-field">
|
|
274
282
|
<span className="sc-field-label">{form.skillsAddress}</span>
|
|
275
|
-
<input
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
283
|
+
<span className="sc-input-group">
|
|
284
|
+
{props.skillsSource !== undefined && (
|
|
285
|
+
<span className="sc-input-prefix">{props.skillsSource}</span>
|
|
286
|
+
)}
|
|
287
|
+
<input
|
|
288
|
+
className="sc-input"
|
|
289
|
+
value={skillsAddress}
|
|
290
|
+
maxLength={MAX_SKILLS_ADDRESS_LENGTH}
|
|
291
|
+
placeholder={form.skillsAddressPlaceholder}
|
|
292
|
+
onChange={(event) => setSkillsAddress(event.target.value)}
|
|
293
|
+
/>
|
|
294
|
+
</span>
|
|
295
|
+
<span className="sc-muted">
|
|
296
|
+
{form.skillsAddressHint(props.organizationAddress ?? null)}
|
|
297
|
+
</span>
|
|
282
298
|
</label>
|
|
283
299
|
</div>
|
|
284
300
|
<div className="sc-field-row sc-mark-row">
|
|
@@ -358,6 +374,8 @@ export interface MemberListProps {
|
|
|
358
374
|
readonly onAdd?: ((input: RestMemberInput) => void) | undefined;
|
|
359
375
|
readonly onChangeRole?: ((member: RestMember, role: ProjectRole) => void) | undefined;
|
|
360
376
|
readonly onRemove?: ((member: RestMember) => void) | undefined;
|
|
377
|
+
/** Where a member's work is seen; given to the project's owners. */
|
|
378
|
+
readonly workHref?: ((member: RestMember) => string) | undefined;
|
|
361
379
|
readonly busy?: boolean | undefined;
|
|
362
380
|
readonly empty?: ReactNode;
|
|
363
381
|
}
|
|
@@ -411,6 +429,11 @@ export function MemberList(props: MemberListProps) {
|
|
|
411
429
|
</select>
|
|
412
430
|
</label>
|
|
413
431
|
)}
|
|
432
|
+
{props.workHref !== undefined && (
|
|
433
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.workHref(member)}>
|
|
434
|
+
{words.people.work}
|
|
435
|
+
</a>
|
|
436
|
+
)}
|
|
414
437
|
{props.onRemove !== undefined && (
|
|
415
438
|
<Button
|
|
416
439
|
variant="danger"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { type FormEvent, useState } from "react";
|
|
2
|
+
import { fillRules, isReferenceRules, MAX_RULES_LENGTH, type RestRules } from "../api.js";
|
|
3
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
4
|
+
import { Writer } from "./documents.js";
|
|
5
|
+
import { Markdown } from "./markdown.js";
|
|
6
|
+
import { Button, Callout, Time } from "./ui.js";
|
|
7
|
+
|
|
8
|
+
// The rules of the board (ADR-0023), as the settings pages show them: what holds, where it
|
|
9
|
+
// comes from (the console's own, a page written, or none), who wrote the page and when, and
|
|
10
|
+
// the ways to change them for whoever may. Each takes its data as props and nothing from the
|
|
11
|
+
// network; the rules are Markdown rendered to elements, never HTML, with `{language}` filled
|
|
12
|
+
// with the language the pages speak, as the command fills it with the person's.
|
|
13
|
+
|
|
14
|
+
export interface RulesViewProps {
|
|
15
|
+
readonly rules: RestRules;
|
|
16
|
+
/** Whose rules: the workspace's, or a project's, which add to the workspace's. */
|
|
17
|
+
readonly scope: "workspace" | "project";
|
|
18
|
+
/** Where the rules are written; left out, they are read here and not changed. */
|
|
19
|
+
readonly editHref?: string | undefined;
|
|
20
|
+
/** Called to put the written rules away; left out, they stay. */
|
|
21
|
+
readonly onArchive?: (() => void) | undefined;
|
|
22
|
+
readonly busy?: boolean | undefined;
|
|
23
|
+
/** Where a page of the project is read, by its path, for the links a project's rules make. */
|
|
24
|
+
readonly docHref?: ((path: string) => string) | undefined;
|
|
25
|
+
/** Whether the text stays folded under where it comes from, on a page where it is not the point. */
|
|
26
|
+
readonly folded?: boolean | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function RulesView(props: RulesViewProps) {
|
|
30
|
+
const words = useWords();
|
|
31
|
+
const language = useLanguage().tag;
|
|
32
|
+
const { rules, scope } = props;
|
|
33
|
+
const page = rules.page;
|
|
34
|
+
if (rules.source === "none" || rules.body === null) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="sc-rules">
|
|
37
|
+
<p className="sc-muted">{words.rules.none}</p>
|
|
38
|
+
{props.editHref !== undefined && (
|
|
39
|
+
<div className="sc-rules-actions">
|
|
40
|
+
<a className="sc-button sc-button-secondary" href={props.editHref}>
|
|
41
|
+
{words.rules.write}
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const text = <Markdown source={fillRules(rules.body, language)} docHref={props.docHref} />;
|
|
49
|
+
return (
|
|
50
|
+
<div className="sc-rules">
|
|
51
|
+
{rules.source === "reference" ? (
|
|
52
|
+
<Callout tone="info">{words.rules.reference}</Callout>
|
|
53
|
+
) : (
|
|
54
|
+
page !== null && (
|
|
55
|
+
<p className="sc-rules-facts">
|
|
56
|
+
{words.docs.version(page.version)} · {words.docs.writtenBy}{" "}
|
|
57
|
+
<Writer by={page.updatedBy} agent={page.agent} /> <Time iso={page.updatedAt} />
|
|
58
|
+
</p>
|
|
59
|
+
)
|
|
60
|
+
)}
|
|
61
|
+
{props.folded === true ? (
|
|
62
|
+
<details className="sc-rules-fold">
|
|
63
|
+
<summary>{words.rules.read}</summary>
|
|
64
|
+
{text}
|
|
65
|
+
</details>
|
|
66
|
+
) : (
|
|
67
|
+
text
|
|
68
|
+
)}
|
|
69
|
+
{(props.editHref !== undefined || props.onArchive !== undefined) && (
|
|
70
|
+
<div className="sc-rules-actions">
|
|
71
|
+
{props.editHref !== undefined && (
|
|
72
|
+
<a className="sc-button sc-button-secondary" href={props.editHref}>
|
|
73
|
+
{rules.source === "reference" ? words.rules.writeOrganizations : words.common.edit}
|
|
74
|
+
</a>
|
|
75
|
+
)}
|
|
76
|
+
{rules.source === "written" && props.onArchive !== undefined && (
|
|
77
|
+
<Button variant="ghost" onClick={props.onArchive} disabled={props.busy === true}>
|
|
78
|
+
{scope === "workspace" ? words.rules.useReference : words.common.remove}
|
|
79
|
+
</Button>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RulesFormProps {
|
|
88
|
+
/** The rules as they hold now: what the form starts from when a page is written. */
|
|
89
|
+
readonly rules: RestRules;
|
|
90
|
+
readonly scope: "workspace" | "project";
|
|
91
|
+
/** What the form starts from when a project adds none yet: the headings, and nothing an agent could mistake for a rule. */
|
|
92
|
+
readonly template: string;
|
|
93
|
+
readonly busy?: boolean | undefined;
|
|
94
|
+
readonly error?: string | undefined;
|
|
95
|
+
readonly onSubmit: (input: {
|
|
96
|
+
readonly body: string;
|
|
97
|
+
readonly baseVersion?: number | undefined;
|
|
98
|
+
}) => void;
|
|
99
|
+
readonly onCancel?: (() => void) | undefined;
|
|
100
|
+
/** Called to put the organization's page away, so that the console's own rules hold again; offered while one is written. */
|
|
101
|
+
readonly onReset?: (() => void) | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The form the rules are written on: prefilled with what holds (the console's own, for a
|
|
106
|
+
* workspace that wrote none), and saving nothing that would change nothing: the text as it
|
|
107
|
+
* is, or the console's own rules where they hold already. Where the organization's are
|
|
108
|
+
* written, the console's own saved as they stand put the page away, as the reset does.
|
|
109
|
+
*/
|
|
110
|
+
export function RulesForm(props: RulesFormProps) {
|
|
111
|
+
const words = useWords();
|
|
112
|
+
const { rules, scope } = props;
|
|
113
|
+
/** What holds now: the page written, or the console's own; nothing for a project that adds none. */
|
|
114
|
+
const current = rules.source === "none" ? undefined : (rules.body ?? undefined);
|
|
115
|
+
const [body, setBody] = useState(current ?? props.template);
|
|
116
|
+
const unchanged =
|
|
117
|
+
body.trim() === (current ?? "").trim() ||
|
|
118
|
+
(scope === "workspace" && rules.source === "reference" && isReferenceRules(body));
|
|
119
|
+
const ready = !unchanged && body.trim().length > 0;
|
|
120
|
+
|
|
121
|
+
const submit = (event: FormEvent) => {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
if (!ready) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
props.onSubmit({
|
|
127
|
+
body,
|
|
128
|
+
...(rules.page === null ? {} : { baseVersion: rules.page.version }),
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<form className="sc-form" onSubmit={submit}>
|
|
134
|
+
<label className="sc-field">
|
|
135
|
+
<span className="sc-field-label">{words.rules.form.body}</span>
|
|
136
|
+
<textarea
|
|
137
|
+
className="sc-input sc-textarea sc-doc-editor"
|
|
138
|
+
value={body}
|
|
139
|
+
maxLength={MAX_RULES_LENGTH}
|
|
140
|
+
rows={20}
|
|
141
|
+
onChange={(event) => setBody(event.target.value)}
|
|
142
|
+
/>
|
|
143
|
+
<span className="sc-muted">{words.rules.form.hint(MAX_RULES_LENGTH)}</span>
|
|
144
|
+
</label>
|
|
145
|
+
{props.error !== undefined && (
|
|
146
|
+
<p className="sc-form-error" role="alert">
|
|
147
|
+
{props.error}
|
|
148
|
+
</p>
|
|
149
|
+
)}
|
|
150
|
+
<div className="sc-form-actions">
|
|
151
|
+
<Button type="submit" variant="primary" disabled={props.busy === true || !ready}>
|
|
152
|
+
{words.rules.form.save}
|
|
153
|
+
</Button>
|
|
154
|
+
{props.onCancel !== undefined && (
|
|
155
|
+
<Button variant="ghost" onClick={props.onCancel}>
|
|
156
|
+
{words.common.cancel}
|
|
157
|
+
</Button>
|
|
158
|
+
)}
|
|
159
|
+
{props.onReset !== undefined && rules.source === "written" && (
|
|
160
|
+
<Button variant="ghost" onClick={props.onReset} disabled={props.busy === true}>
|
|
161
|
+
{words.rules.useReference}
|
|
162
|
+
</Button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</form>
|
|
166
|
+
);
|
|
167
|
+
}
|
package/src/components/runs.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { agentLabel, type RestArtifact, type RestRun, type RunStatus } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
4
|
import { Markdown } from "./markdown.js";
|
|
5
5
|
import { Badge, Button, formatBytes, PersonChip, personName, Time } from "./ui.js";
|
|
@@ -51,7 +51,9 @@ export function RunCard(props: RunCardProps) {
|
|
|
51
51
|
return (
|
|
52
52
|
<article className="sc-run" aria-label={words.label(run.agent, personName(run.person))}>
|
|
53
53
|
<header className="sc-run-header">
|
|
54
|
-
<span className="sc-run-agent"
|
|
54
|
+
<span className="sc-run-agent" title={run.agent}>
|
|
55
|
+
{agentLabel(run.agent)}
|
|
56
|
+
</span>
|
|
55
57
|
<RunStatusBadge status={run.status} />
|
|
56
58
|
<span className="sc-run-meta">
|
|
57
59
|
{words.for} <PersonChip person={run.person} /> {words.since} <Time iso={run.startedAt} />
|
|
@@ -17,20 +17,32 @@ export function SkillList(props: SkillListProps) {
|
|
|
17
17
|
if (skills === undefined) {
|
|
18
18
|
return <Spinner label={words.loading} />;
|
|
19
19
|
}
|
|
20
|
+
// The console could not answer for the deployment at all: said as such, not as no address.
|
|
21
|
+
if (skills.status === "unavailable" && skills.address === null) {
|
|
22
|
+
return <Callout tone="warning">{words.status.unavailable}</Callout>;
|
|
23
|
+
}
|
|
20
24
|
if (skills.status === "none" || skills.address === null || skills.page === null) {
|
|
21
25
|
return <EmptyState title={words.noAddress.title} body={words.noAddress.body} />;
|
|
22
26
|
}
|
|
23
27
|
const where = (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
<>
|
|
29
|
+
<p className="sc-lead">
|
|
30
|
+
{words.servedBy}{" "}
|
|
31
|
+
<a href={skills.source} target="_blank" rel="noopener noreferrer">
|
|
32
|
+
SkillCDN
|
|
33
|
+
</a>{" "}
|
|
34
|
+
{words.at}{" "}
|
|
35
|
+
<a href={skills.page} target="_blank" rel="noopener noreferrer">
|
|
36
|
+
<code>{skills.address}</code>
|
|
37
|
+
</a>
|
|
38
|
+
</p>
|
|
39
|
+
<p className="sc-muted">
|
|
40
|
+
{words.connect}{" "}
|
|
41
|
+
<a href={skills.page} target="_blank" rel="noopener noreferrer">
|
|
42
|
+
{words.connectLink}
|
|
43
|
+
</a>
|
|
44
|
+
</p>
|
|
45
|
+
</>
|
|
34
46
|
);
|
|
35
47
|
if (skills.status !== "ready") {
|
|
36
48
|
return (
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type RestTokenInput,
|
|
7
7
|
} from "../api.js";
|
|
8
8
|
import { useWords } from "../i18n/index.js";
|
|
9
|
-
import { Button, Callout, Time } from "./ui.js";
|
|
9
|
+
import { Button, Callout, CopyButton, Time } from "./ui.js";
|
|
10
10
|
|
|
11
11
|
// The tokens a person holds for their agents, scripts and consoles of their own, shown as the
|
|
12
12
|
// agents that hold them: the list, the way to make one by hand, and the one just made, whose
|
|
@@ -180,16 +180,6 @@ export interface NewTokenProps {
|
|
|
180
180
|
export function NewToken(props: NewTokenProps) {
|
|
181
181
|
const words = useWords();
|
|
182
182
|
const fresh = words.agents.fresh;
|
|
183
|
-
const [copied, setCopied] = useState(false);
|
|
184
|
-
const copy = () => {
|
|
185
|
-
if (typeof navigator === "undefined" || navigator.clipboard === undefined) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
void navigator.clipboard.writeText(props.secret).then(
|
|
189
|
-
() => setCopied(true),
|
|
190
|
-
() => setCopied(false),
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
183
|
return (
|
|
194
184
|
<Callout
|
|
195
185
|
tone="warning"
|
|
@@ -203,9 +193,7 @@ export function NewToken(props: NewTokenProps) {
|
|
|
203
193
|
<p>{fresh.copyNow}</p>
|
|
204
194
|
<p className="sc-secret">
|
|
205
195
|
<code className="sc-secret-value">{props.secret}</code>
|
|
206
|
-
<
|
|
207
|
-
{copied ? fresh.copied : fresh.copy}
|
|
208
|
-
</Button>
|
|
196
|
+
<CopyButton text={props.secret} size="sm" />
|
|
209
197
|
</p>
|
|
210
198
|
<p>
|
|
211
199
|
{fresh.useIt}{" "}
|