@hanzo/build 0.2.34 → 0.2.36
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/lib/api/github.d.ts +13 -5
- package/lib/api/github.js +21 -8
- package/lib/api/provider.d.ts +3 -3
- package/lib/api/provider.js +4 -4
- package/lib/back.d.ts +29 -0
- package/lib/back.js +82 -0
- package/lib/project.js +60 -12
- package/lib/settings/integrations.js +13 -5
- package/package.json +1 -1
package/lib/api/github.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* GET /v1/provider/github/repos?q=&owner=&limit=&after= most recently pushed first
|
|
5
5
|
* GET /v1/provider/github/repos/{owner}/{repo}/branches?q=&limit=&after= default first
|
|
6
6
|
* GET /v1/provider/github/user the person's own connection
|
|
7
|
-
* POST /v1/provider/github/user/connect → {authorizeUrl}
|
|
7
|
+
* POST /v1/provider/github/user/connect {return} → {authorizeUrl}
|
|
8
|
+
* POST /v1/provider/github/user/complete {grant} → the connection
|
|
8
9
|
* POST /v1/provider/github/user/disconnect
|
|
9
10
|
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
10
11
|
*
|
|
@@ -14,8 +15,9 @@
|
|
|
14
15
|
*
|
|
15
16
|
* `connected` false with repositories is an org admin seeing the org's
|
|
16
17
|
* installations; false with none is the cue to connect. GitHub returns a
|
|
17
|
-
* connecting person to the
|
|
18
|
-
*
|
|
18
|
+
* connecting person to the page the connect named as its return (a Hanzo app
|
|
19
|
+
* page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
|
|
20
|
+
* and that page completes it (`complete`, and `landed` in ../back.ts).
|
|
19
21
|
*/
|
|
20
22
|
import { type Target } from './call.ts';
|
|
21
23
|
export interface Repo {
|
|
@@ -74,8 +76,14 @@ export interface BranchQuery {
|
|
|
74
76
|
}
|
|
75
77
|
export declare function branches(t: Target, owner: string, name: string, p?: BranchQuery, signal?: AbortSignal): Promise<Branches>;
|
|
76
78
|
export declare function connection(t: Target): Promise<Connection>;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Where to send the person to authorize the platform's GitHub App — github.com
|
|
81
|
+
* only. `back` is the page GitHub's return lands on; the platform keeps it only
|
|
82
|
+
* when it is a Hanzo app page.
|
|
83
|
+
*/
|
|
84
|
+
export declare function connect(t: Target, back?: string): Promise<string>;
|
|
85
|
+
/** Finish the connect GitHub's return handed this page as `grant`. */
|
|
86
|
+
export declare function complete(t: Target, grant: string): Promise<Connection>;
|
|
79
87
|
export declare function disconnect(t: Target): Promise<void>;
|
|
80
88
|
/** A GitHub account the platform's App is installed on, as this org sees it. */
|
|
81
89
|
export interface Installation {
|
package/lib/api/github.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* GET /v1/provider/github/repos?q=&owner=&limit=&after= most recently pushed first
|
|
5
5
|
* GET /v1/provider/github/repos/{owner}/{repo}/branches?q=&limit=&after= default first
|
|
6
6
|
* GET /v1/provider/github/user the person's own connection
|
|
7
|
-
* POST /v1/provider/github/user/connect → {authorizeUrl}
|
|
7
|
+
* POST /v1/provider/github/user/connect {return} → {authorizeUrl}
|
|
8
|
+
* POST /v1/provider/github/user/complete {grant} → the connection
|
|
8
9
|
* POST /v1/provider/github/user/disconnect
|
|
9
10
|
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
10
11
|
*
|
|
@@ -14,8 +15,9 @@
|
|
|
14
15
|
*
|
|
15
16
|
* `connected` false with repositories is an org admin seeing the org's
|
|
16
17
|
* installations; false with none is the cue to connect. GitHub returns a
|
|
17
|
-
* connecting person to the
|
|
18
|
-
*
|
|
18
|
+
* connecting person to the page the connect named as its return (a Hanzo app
|
|
19
|
+
* page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
|
|
20
|
+
* and that page completes it (`complete`, and `landed` in ../back.ts).
|
|
19
21
|
*/
|
|
20
22
|
import { call, query, seg } from './call.js';
|
|
21
23
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
@@ -57,18 +59,29 @@ export async function branches(t, owner, name, p = {}, signal) {
|
|
|
57
59
|
total: num(raw.total),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
62
|
+
function own(raw) {
|
|
63
|
+
const c = obj(raw);
|
|
64
|
+
return { configured: c.configured === true, connected: c.connected === true, login: str(c.login) };
|
|
65
|
+
}
|
|
60
66
|
export async function connection(t) {
|
|
61
|
-
|
|
62
|
-
return { configured: raw.configured === true, connected: raw.connected === true, login: str(raw.login) };
|
|
67
|
+
return own(await call(t, 'GET', '/v1/provider/github/user'));
|
|
63
68
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Where to send the person to authorize the platform's GitHub App — github.com
|
|
71
|
+
* only. `back` is the page GitHub's return lands on; the platform keeps it only
|
|
72
|
+
* when it is a Hanzo app page.
|
|
73
|
+
*/
|
|
74
|
+
export async function connect(t, back) {
|
|
75
|
+
const raw = obj(await call(t, 'POST', '/v1/provider/github/user/connect', back ? { return: back } : undefined));
|
|
67
76
|
const url = str(raw.authorizeUrl);
|
|
68
77
|
if (!/^https:\/\/github\.com\//.test(url))
|
|
69
78
|
throw new Error('The platform did not name a GitHub address to connect at');
|
|
70
79
|
return url;
|
|
71
80
|
}
|
|
81
|
+
/** Finish the connect GitHub's return handed this page as `grant`. */
|
|
82
|
+
export async function complete(t, grant) {
|
|
83
|
+
return own(await call(t, 'POST', '/v1/provider/github/user/complete', { grant }));
|
|
84
|
+
}
|
|
72
85
|
export async function disconnect(t) {
|
|
73
86
|
await call(t, 'POST', '/v1/provider/github/user/disconnect');
|
|
74
87
|
}
|
package/lib/api/provider.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* platform's GitHub App is installed on.
|
|
4
4
|
*
|
|
5
5
|
* GET /v1/provider/{id} one connector and this org's connection to it
|
|
6
|
-
* POST /v1/provider/{id}/connect {} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
6
|
+
* POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
7
7
|
* POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
|
|
8
8
|
* GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
|
|
9
9
|
*
|
|
@@ -25,8 +25,8 @@ export interface Connector {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function connector(raw: unknown, id?: string): Connector;
|
|
27
27
|
export declare function read(t: Target, id: string): Promise<Connector>;
|
|
28
|
-
/** The provider's consent page for this org. */
|
|
29
|
-
export declare function authorize(t: Target, id: string): Promise<string>;
|
|
28
|
+
/** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
|
|
29
|
+
export declare function authorize(t: Target, id: string, back?: string): Promise<string>;
|
|
30
30
|
export declare function disconnect(t: Target, id: string): Promise<void>;
|
|
31
31
|
export interface Channel {
|
|
32
32
|
id: string;
|
package/lib/api/provider.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* platform's GitHub App is installed on.
|
|
4
4
|
*
|
|
5
5
|
* GET /v1/provider/{id} one connector and this org's connection to it
|
|
6
|
-
* POST /v1/provider/{id}/connect {} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
6
|
+
* POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
7
7
|
* POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
|
|
8
8
|
* GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
|
|
9
9
|
*
|
|
@@ -35,9 +35,9 @@ const CONSENT = {
|
|
|
35
35
|
slack: /^https:\/\/slack\.com\//,
|
|
36
36
|
github: /^https:\/\/github\.com\//,
|
|
37
37
|
};
|
|
38
|
-
/** The provider's consent page for this org. */
|
|
39
|
-
export async function authorize(t, id) {
|
|
40
|
-
const url = str(obj(await call(t, 'POST', `/v1/provider/${seg(id)}/connect`, {})).authorizeUrl);
|
|
38
|
+
/** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
|
|
39
|
+
export async function authorize(t, id, back) {
|
|
40
|
+
const url = str(obj(await call(t, 'POST', `/v1/provider/${seg(id)}/connect`, back ? { return: back } : {})).authorizeUrl);
|
|
41
41
|
const host = CONSENT[id];
|
|
42
42
|
if (!host || !host.test(url))
|
|
43
43
|
throw new Error(`The platform did not name a ${id} address to connect at`);
|
package/lib/back.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Target } from './api/call.ts';
|
|
2
|
+
/** The query keys a return carries its answer in. */
|
|
3
|
+
export declare const ANSWER: readonly ['complete', 'grant', 'connected', 'account', 'error', 'reason'];
|
|
4
|
+
export type Landing = {
|
|
5
|
+
kind: 'grant';
|
|
6
|
+
provider: string;
|
|
7
|
+
grant: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'connected';
|
|
10
|
+
provider: string;
|
|
11
|
+
account: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'error';
|
|
14
|
+
provider: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
/** The answer a return carried in `search`, or null when it carried none. */
|
|
18
|
+
export declare function landed(search: string): Landing | null;
|
|
19
|
+
/** `href` without the answer keys; `href` itself when it carries none. */
|
|
20
|
+
export declare function clean(href: string): string;
|
|
21
|
+
/** This page's address, as a connect's return. */
|
|
22
|
+
export declare function here(): string;
|
|
23
|
+
/** A provider id as people write it. */
|
|
24
|
+
export declare const named: (id: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Finish what a return carried, once a person is signed in: `say` gets the
|
|
27
|
+
* sentence, and `after` runs once something was connected.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useBack(t: Target, signed: boolean, say: (note: string) => void, after: () => void): void;
|
package/lib/back.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coming back from a provider's consent page.
|
|
3
|
+
*
|
|
4
|
+
* A connect names the page it starts on as its return (`here`). The platform
|
|
5
|
+
* keeps that address when it is a Hanzo app page, and its callback lands the
|
|
6
|
+
* person back on it with the answer in the query:
|
|
7
|
+
*
|
|
8
|
+
* ?complete=github&grant=<id> a GitHub connect to finish (POST /v1/provider/github/user/complete)
|
|
9
|
+
* ?connected=<provider>&account=<n> an organization's connector, connected
|
|
10
|
+
* ?error=<provider>&reason=<why> either, refused
|
|
11
|
+
*
|
|
12
|
+
* `useBack` reads that answer once, takes it off the address so a reload does
|
|
13
|
+
* not replay it, finishes a GitHub grant, and says what happened.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect } from 'react';
|
|
16
|
+
import { complete } from './api/github.js';
|
|
17
|
+
/** The query keys a return carries its answer in. */
|
|
18
|
+
export const ANSWER = ['complete', 'grant', 'connected', 'account', 'error', 'reason'];
|
|
19
|
+
/** The answer a return carried in `search`, or null when it carried none. */
|
|
20
|
+
export function landed(search) {
|
|
21
|
+
const q = new URLSearchParams(search);
|
|
22
|
+
const error = q.get('error');
|
|
23
|
+
if (error)
|
|
24
|
+
return { kind: 'error', provider: error, reason: q.get('reason') ?? '' };
|
|
25
|
+
const grant = q.get('grant');
|
|
26
|
+
const done = q.get('complete');
|
|
27
|
+
if (done && grant)
|
|
28
|
+
return { kind: 'grant', provider: done, grant };
|
|
29
|
+
const connected = q.get('connected');
|
|
30
|
+
if (connected)
|
|
31
|
+
return { kind: 'connected', provider: connected, account: q.get('account') ?? '' };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** `href` without the answer keys; `href` itself when it carries none. */
|
|
35
|
+
export function clean(href) {
|
|
36
|
+
const url = new URL(href);
|
|
37
|
+
if (!ANSWER.some((k) => url.searchParams.has(k)))
|
|
38
|
+
return href;
|
|
39
|
+
for (const k of ANSWER)
|
|
40
|
+
url.searchParams.delete(k);
|
|
41
|
+
return url.href;
|
|
42
|
+
}
|
|
43
|
+
/** This page's address, as a connect's return. */
|
|
44
|
+
export function here() {
|
|
45
|
+
return clean(window.location.href);
|
|
46
|
+
}
|
|
47
|
+
const NAMES = { github: 'GitHub', slack: 'Slack' };
|
|
48
|
+
/** A provider id as people write it. */
|
|
49
|
+
export const named = (id) => NAMES[id] ?? (id ? id[0].toUpperCase() + id.slice(1) : 'The connection');
|
|
50
|
+
/**
|
|
51
|
+
* Finish what a return carried, once a person is signed in: `say` gets the
|
|
52
|
+
* sentence, and `after` runs once something was connected.
|
|
53
|
+
*/
|
|
54
|
+
export function useBack(t, signed, say, after) {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!signed)
|
|
57
|
+
return;
|
|
58
|
+
const l = landed(window.location.search);
|
|
59
|
+
if (!l)
|
|
60
|
+
return;
|
|
61
|
+
// Off the address first: a grant completes once, and a reload must not try again.
|
|
62
|
+
const next = clean(window.location.href);
|
|
63
|
+
window.history.replaceState(window.history.state, '', next);
|
|
64
|
+
if (l.kind === 'error') {
|
|
65
|
+
say(`${named(l.provider)}: ${l.reason || 'not connected'}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (l.kind === 'connected') {
|
|
69
|
+
say(`${named(l.provider)} is connected${l.account ? ` as ${l.account}` : ''}`);
|
|
70
|
+
after();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (l.provider !== 'github')
|
|
74
|
+
return;
|
|
75
|
+
complete(t, l.grant).then((c) => {
|
|
76
|
+
say(c.connected ? `Connected as @${c.login}` : 'GitHub did not connect; connect again');
|
|
77
|
+
after();
|
|
78
|
+
}, (e) => say(e.message));
|
|
79
|
+
// Once per landing: the address is clean after the first run.
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
}, [signed]);
|
|
82
|
+
}
|
package/lib/project.js
CHANGED
|
@@ -26,7 +26,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
26
26
|
* template taken there reads `building` until its copy is `live` or failed, and
|
|
27
27
|
* is read again until then; a run that publishes what it pushed says so on its
|
|
28
28
|
* final status, which reads the project again and loads the preview again. Its
|
|
29
|
-
* pull request is merged from beside its link.
|
|
29
|
+
* pull request is merged from beside its link. A build that failed, or that has
|
|
30
|
+
* run past STALL, says so in the preview with the platform's reason and offers
|
|
31
|
+
* Publish again, which builds the project's repository at its branch.
|
|
30
32
|
*
|
|
31
33
|
* Derived from the Hanzo App v2 editor (github.com/hanzoai/build-v2), itself
|
|
32
34
|
* derived from OSW Studio and DeepSite (MIT). See NOTICE.
|
|
@@ -41,6 +43,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
41
43
|
import { read as pushed } from './api/changes.js';
|
|
42
44
|
import { start } from './api/coding.js';
|
|
43
45
|
import { blob, tree } from './api/git.js';
|
|
46
|
+
import { declare } from './api/platform.js';
|
|
44
47
|
import { deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
45
48
|
import { list, message, stop } from './api/sessions.js';
|
|
46
49
|
import { decode, outcome, pull, said, who } from './api/turn.js';
|
|
@@ -57,6 +60,8 @@ import { Attach, compose, Dictate, Files } from './tools.js';
|
|
|
57
60
|
const LIVE = new Set(['running', 'paused']);
|
|
58
61
|
/** How often a project being published is read again, ms. */
|
|
59
62
|
const AGAIN = 5000;
|
|
63
|
+
/** How long a build may run before the builder offers to publish again, s. A template's build takes a few minutes. */
|
|
64
|
+
const STALL = 10 * 60;
|
|
60
65
|
/** The chat's width beside the work, px: its default, and the two it is kept between. */
|
|
61
66
|
const CHAT_WIDTH = 360;
|
|
62
67
|
const CHAT_FLOOR = 300;
|
|
@@ -89,14 +94,39 @@ export function Project({ slug }) {
|
|
|
89
94
|
const building = project?.status === 'building';
|
|
90
95
|
const failed = project?.status === 'error';
|
|
91
96
|
const { reload: again } = all;
|
|
97
|
+
// The clock a build is measured against, moved each time the project is read again.
|
|
98
|
+
const [now, setNow] = useState(() => Date.now());
|
|
92
99
|
useEffect(() => {
|
|
93
100
|
if (!building)
|
|
94
101
|
return;
|
|
95
|
-
const every = setInterval(
|
|
102
|
+
const every = setInterval(() => {
|
|
103
|
+
again();
|
|
104
|
+
setNow(Date.now());
|
|
105
|
+
}, AGAIN);
|
|
96
106
|
return () => clearInterval(every);
|
|
97
107
|
}, [building, again]);
|
|
98
|
-
//
|
|
99
|
-
const failure = useRead(failed ? () => deployments(t, slug) : null, [], [t, slug, failed, project?.updated]);
|
|
108
|
+
// When the build began, and why it failed, are its latest deployment's to say.
|
|
109
|
+
const failure = useRead(building || failed ? () => deployments(t, slug) : null, [], [t, slug, building, failed, project?.updated]);
|
|
110
|
+
const latest = failure.value[0];
|
|
111
|
+
const began = latest?.status === 'building' ? latest.created : 0;
|
|
112
|
+
const ran = began ? Math.max(0, Math.floor(now / 1000) - began) : 0;
|
|
113
|
+
const stalled = building && ran > STALL;
|
|
114
|
+
// Publish again: the project's repository, built at its branch, the way Publish builds it.
|
|
115
|
+
const [redo, setRedo] = useState({ busy: false, error: '' });
|
|
116
|
+
const retry = async () => {
|
|
117
|
+
if (!project?.repo || redo.busy)
|
|
118
|
+
return;
|
|
119
|
+
setRedo({ busy: true, error: '' });
|
|
120
|
+
try {
|
|
121
|
+
await declare(t, { repo: project.repo, ref: project.branch || 'main', name: slug, project: slug, mode: 'branch' });
|
|
122
|
+
setNow(Date.now());
|
|
123
|
+
again();
|
|
124
|
+
setRedo({ busy: false, error: '' });
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
setRedo({ busy: false, error: e.message });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
100
130
|
const listed = useRead(signed ? () => list(t, { kind: 'coding', project: slug, limit: 50 }) : null, [], [t, signed, slug]);
|
|
101
131
|
// Only the runs on this project's own repository: a record can be moved into
|
|
102
132
|
// any project, and a run's branch is what Files, Code and Publish read.
|
|
@@ -314,18 +344,36 @@ export function Project({ slug }) {
|
|
|
314
344
|
? 'Reading this project’s runs…'
|
|
315
345
|
: project?.live
|
|
316
346
|
? `${project.name} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.`
|
|
317
|
-
:
|
|
318
|
-
? `${project?.name}
|
|
319
|
-
:
|
|
320
|
-
? `${project?.name}
|
|
321
|
-
:
|
|
322
|
-
? `${project
|
|
323
|
-
:
|
|
347
|
+
: stalled
|
|
348
|
+
? `${project?.name} has been publishing for ${Math.floor(ran / 60)} minutes without finishing. Publish it again from the preview, or say what to change and it gets built.`
|
|
349
|
+
: failed
|
|
350
|
+
? `${project?.name} could not be published. Publish it again from the preview, or say what to change and it gets built.`
|
|
351
|
+
: building
|
|
352
|
+
? `${project?.name} is being published — it appears in the preview when its build finishes. Say what to change and it gets built.`
|
|
353
|
+
: starter
|
|
354
|
+
? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
|
|
355
|
+
: project
|
|
356
|
+
? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
|
|
357
|
+
: `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { ...cut, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
|
|
324
358
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
325
359
|
: undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !collapsed ? (_jsx(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
|
|
326
360
|
// The preview stays loaded while another view is open, so coming back to it
|
|
327
361
|
// is the page as it was left.
|
|
328
|
-
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${project?.name ?? slug} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children:
|
|
362
|
+
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${project?.name ?? slug} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
|
|
363
|
+
? 'Publishing is taking too long'
|
|
364
|
+
: building
|
|
365
|
+
? project?.forked
|
|
366
|
+
? 'Publishing your copy…'
|
|
367
|
+
: 'Publishing…'
|
|
368
|
+
: failed
|
|
369
|
+
? 'Publishing failed'
|
|
370
|
+
: 'Nothing deployed yet' }), _jsx(SizableText, { size: "$2", color: "$soft", text: "center", children: stalled
|
|
371
|
+
? `Its build started ${Math.floor(ran / 60)} minutes ago and has not finished.`
|
|
372
|
+
: building
|
|
373
|
+
? 'Its page appears here when the build finishes.'
|
|
374
|
+
: failed
|
|
375
|
+
? why
|
|
376
|
+
: 'Publish this project and its page appears here.' }), (stalled || failed) && project?.repo ? (_jsx(Button, { variant: "outline", disabled: redo.busy, onPress: () => void retry(), children: "Publish again" })) : null, redo.error ? (_jsx(SizableText, { size: "$2", color: "$soft", text: "center", role: "alert", children: redo.error })) : null] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
|
|
329
377
|
const next = !editing;
|
|
330
378
|
setEditing(next);
|
|
331
379
|
frame.current?.post({ type: 'preview:editable', active: next });
|
|
@@ -3,9 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* Integrations: your own GitHub connection, the GitHub accounts the
|
|
4
4
|
* organization has installed the platform's App on, and its Slack workspace.
|
|
5
5
|
*
|
|
6
|
-
* Connecting leaves this page once, for GitHub's or Slack's own consent screen
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Connecting leaves this page once, for GitHub's or Slack's own consent screen,
|
|
7
|
+
* and the platform's callback brings the person back here with the answer
|
|
8
|
+
* (../back.ts), which this page finishes and reports. Anyone connects their own
|
|
9
|
+
* GitHub; installing the App and connecting Slack are an org admin's, which is
|
|
10
|
+
* the platform's rule.
|
|
9
11
|
*/
|
|
10
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
13
|
import { Github, Hash, Lock, Slack } from '@hanzogui/lucide-icons-2';
|
|
@@ -13,6 +15,7 @@ import { Button } from '@hanzo/ui';
|
|
|
13
15
|
import { useState } from 'react';
|
|
14
16
|
import { connect, connection, disconnect as unlinkGithub, installations } from '../api/github.js';
|
|
15
17
|
import { authorize, channels, disconnect, read } from '../api/provider.js';
|
|
18
|
+
import { here, useBack } from '../back.js';
|
|
16
19
|
import { useRead } from '../data.js';
|
|
17
20
|
import { useHost, useTarget } from '../host.js';
|
|
18
21
|
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
@@ -27,6 +30,11 @@ export function Integrations() {
|
|
|
27
30
|
const rooms = useRead(signed && joined ? () => channels(t) : null, { channels: [], next: '' }, [t, signed, joined]);
|
|
28
31
|
const [working, setWorking] = useState(false);
|
|
29
32
|
const [note, setNote] = useState('');
|
|
33
|
+
useBack(t, signed, setNote, () => {
|
|
34
|
+
mine.reload();
|
|
35
|
+
installed.reload();
|
|
36
|
+
slack.reload();
|
|
37
|
+
});
|
|
30
38
|
if (!signed) {
|
|
31
39
|
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Integrations" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s integrations." })] }));
|
|
32
40
|
}
|
|
@@ -67,7 +75,7 @@ export function Integrations() {
|
|
|
67
75
|
? 'This deployment cannot connect GitHub yet.'
|
|
68
76
|
: me.connected
|
|
69
77
|
? `Connected as @${me.login}`
|
|
70
|
-
: 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(t), 'Your GitHub is disconnected', mine.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(t)), children: "Connect" }))) : null }), installed.error ? (_jsx(Row, { title: "App installations", detail: installed.error.message })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? 'every repository' : i.grant === 'selected' ? 'selected repositories' : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? 'Installed' : 'Removed on GitHub' }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: "No GitHub accounts installed", detail: "Install the App on a GitHub account to bring its repositories here." })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'github')), children: "Install on a GitHub account" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin installs the App on a GitHub account." }))] }), _jsxs(Group, { title: "Slack", detail: "Talk to Hanzo from your workspace\u2019s channels.", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
|
|
78
|
+
: 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(t), 'Your GitHub is disconnected', mine.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(t, here())), children: "Connect" }))) : null }), installed.error ? (_jsx(Row, { title: "App installations", detail: installed.error.message })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? 'every repository' : i.grant === 'selected' ? 'selected repositories' : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? 'Installed' : 'Removed on GitHub' }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: "No GitHub accounts installed", detail: "Install the App on a GitHub account to bring its repositories here." })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'github', here())), children: "Install on a GitHub account" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin installs the App on a GitHub account." }))] }), _jsxs(Group, { title: "Slack", detail: "Talk to Hanzo from your workspace\u2019s channels.", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
|
|
71
79
|
? slack.error.message
|
|
72
80
|
: !s
|
|
73
81
|
? 'Reading…'
|
|
@@ -75,7 +83,7 @@ export function Integrations() {
|
|
|
75
83
|
? `Connected${s.since ? ` since ${day(s.since)}` : ''}`
|
|
76
84
|
: s.available
|
|
77
85
|
? 'Not connected'
|
|
78
|
-
: s.note || 'This deployment cannot connect Slack yet.', trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack')), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
|
|
86
|
+
: s.note || 'This deployment cannot connect Slack yet.', trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack', here())), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
|
|
79
87
|
? rooms.value.channels.map((c) => (_jsx(Row, { leading: c.private ? _jsx(Lock, { size: 14 }) : _jsx(Hash, { size: 14 }), title: c.name || c.id, trailing: _jsx(SizableText, { size: "$1", color: c.member ? '$ink' : '$soft', children: c.member ? 'Joined' : 'Not joined' }) }, c.id)))
|
|
80
88
|
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the Slack workspace." })) : null] }), _jsx(Note, { children: note })] }));
|
|
81
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|