@peers-app/peers-ui 0.25.9 → 0.25.11
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/dist/screens/packages/local-folder-grants-panel.d.ts +15 -0
- package/dist/screens/packages/local-folder-grants-panel.js +131 -0
- package/dist/screens/packages/local-folders-ipc.d.ts +24 -0
- package/dist/screens/packages/local-folders-ipc.js +10 -0
- package/dist/screens/packages/package-details.js +5 -0
- package/dist/screens/packages/package-list.d.ts +1 -0
- package/dist/screens/packages/package-list.js +3 -1
- package/package.json +3 -3
- package/src/screens/packages/local-folder-grants-panel.tsx +290 -0
- package/src/screens/packages/local-folders-ipc.ts +28 -0
- package/src/screens/packages/package-details.tsx +9 -0
- package/src/screens/packages/package-list.tsx +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Props for {@link LocalFolderGrantsPanel}. */
|
|
2
|
+
export interface ILocalFolderGrantsPanelProps {
|
|
3
|
+
/**
|
|
4
|
+
* When set, the list and new grants are limited to this package.
|
|
5
|
+
* Omit it for the device-wide operator list.
|
|
6
|
+
*/
|
|
7
|
+
packageId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Folder grants on this device, with add and revoke.
|
|
11
|
+
*
|
|
12
|
+
* Add opens the host folder picker. A host that does not provide System Local Folders
|
|
13
|
+
* (the PWA) shows the contract as unavailable.
|
|
14
|
+
*/
|
|
15
|
+
export declare function LocalFolderGrantsPanel(props: ILocalFolderGrantsPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalFolderGrantsPanel = LocalFolderGrantsPanel;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const globals_1 = require("../../globals");
|
|
7
|
+
const hooks_1 = require("../../hooks");
|
|
8
|
+
const use_host_contract_1 = require("../../hooks/use-host-contract");
|
|
9
|
+
const local_folders_ipc_1 = require("./local-folders-ipc");
|
|
10
|
+
/**
|
|
11
|
+
* Folder grants on this device, with add and revoke.
|
|
12
|
+
*
|
|
13
|
+
* Add opens the host folder picker. A host that does not provide System Local Folders
|
|
14
|
+
* (the PWA) shows the contract as unavailable.
|
|
15
|
+
*/
|
|
16
|
+
function LocalFolderGrantsPanel(props) {
|
|
17
|
+
const { consumer, error: transportError } = (0, use_host_contract_1.useHostContract)(local_folders_ipc_1.localFoldersConsumer);
|
|
18
|
+
const [groupList] = (0, hooks_1.useObservable)(globals_1.groups);
|
|
19
|
+
const [grants, setGrants] = (0, react_1.useState)();
|
|
20
|
+
const [loadError, setLoadError] = (0, react_1.useState)();
|
|
21
|
+
const [actionError, setActionError] = (0, react_1.useState)();
|
|
22
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
23
|
+
const [groupId, setGroupId] = (0, react_1.useState)("");
|
|
24
|
+
const [mode, setMode] = (0, react_1.useState)("readwrite");
|
|
25
|
+
const [refresh, setRefresh] = (0, react_1.useState)(0);
|
|
26
|
+
(0, react_1.useEffect)(() => {
|
|
27
|
+
if (!consumer) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
consumer.tools.listGrants().then((result) => {
|
|
32
|
+
if (!cancelled) {
|
|
33
|
+
setGrants(result.grants);
|
|
34
|
+
setLoadError(undefined);
|
|
35
|
+
}
|
|
36
|
+
}, (cause) => {
|
|
37
|
+
if (!cancelled) {
|
|
38
|
+
setLoadError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [consumer, refresh]);
|
|
45
|
+
const unavailable = isUnavailable(transportError) || isUnavailable(loadError);
|
|
46
|
+
const visible = (grants ?? []).filter((grant) => props.packageId ? grant.packageId === props.packageId : true);
|
|
47
|
+
const names = new Map(groupList.map((group) => [group.groupId, group.name || group.groupId]));
|
|
48
|
+
async function addFolder() {
|
|
49
|
+
if (!consumer || !groupId) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setBusy(true);
|
|
53
|
+
setActionError(undefined);
|
|
54
|
+
try {
|
|
55
|
+
await consumer.tools.requestGrant({
|
|
56
|
+
groupId,
|
|
57
|
+
mode,
|
|
58
|
+
...(props.packageId ? { packageId: props.packageId } : {}),
|
|
59
|
+
});
|
|
60
|
+
setRefresh((value) => value + 1);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
64
|
+
if (!/cancelled/i.test(message)) {
|
|
65
|
+
setActionError(message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
setBusy(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function revoke(grantId) {
|
|
73
|
+
if (!consumer) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!window.confirm("Revoke this folder grant?")) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setBusy(true);
|
|
80
|
+
setActionError(undefined);
|
|
81
|
+
try {
|
|
82
|
+
await consumer.tools.revokeGrant({ grantId });
|
|
83
|
+
setRefresh((value) => value + 1);
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
setActionError(cause instanceof Error ? cause.message : String(cause));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
setBusy(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h5", { children: props.packageId ? "Folders" : "Local folders" }), (0, jsx_runtime_1.jsx)(FolderStatus, { unavailable: unavailable, transportError: transportError, loadError: loadError, loading: !!consumer && grants === undefined && !loadError }), consumer && !unavailable && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(GrantTable, { grants: visible, names: names, busy: busy, onRevoke: revoke }), (0, jsx_runtime_1.jsx)(AddFolderForm, { fieldScope: props.packageId ?? "device", groupId: groupId, groups: groupList.map((group) => ({
|
|
93
|
+
groupId: group.groupId,
|
|
94
|
+
name: group.name || group.groupId,
|
|
95
|
+
})), mode: mode, busy: busy, onGroupId: setGroupId, onMode: setMode, onAdd: addFolder }), actionError && (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger mt-2 mb-0", children: actionError })] }))] }));
|
|
96
|
+
}
|
|
97
|
+
function FolderStatus(props) {
|
|
98
|
+
if (props.unavailable) {
|
|
99
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-secondary mb-0", children: "System Local Folders is not available on this host." }));
|
|
100
|
+
}
|
|
101
|
+
const error = props.loadError ?? props.transportError;
|
|
102
|
+
if (error) {
|
|
103
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger mb-0", children: error.message });
|
|
104
|
+
}
|
|
105
|
+
if (props.loading) {
|
|
106
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "text-muted", children: "Loading folder grants\u2026" });
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function GrantTable(props) {
|
|
111
|
+
if (props.grants.length === 0) {
|
|
112
|
+
return (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "No folder grants." });
|
|
113
|
+
}
|
|
114
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm align-middle", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Path" }), (0, jsx_runtime_1.jsx)("th", { children: "Group" }), (0, jsx_runtime_1.jsx)("th", { children: "Package" }), (0, jsx_runtime_1.jsx)("th", { children: "Mode" }), (0, jsx_runtime_1.jsx)("th", { children: "Created" }), (0, jsx_runtime_1.jsx)("th", {})] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: props.grants.map((grant) => ((0, jsx_runtime_1.jsx)(GrantRow, { grant: grant, groupName: props.names.get(grant.groupId) ?? grant.groupId, busy: props.busy, onRevoke: props.onRevoke }, grant.grantId))) })] }) }));
|
|
115
|
+
}
|
|
116
|
+
function GrantRow(props) {
|
|
117
|
+
const { grant } = props;
|
|
118
|
+
return ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "text-break", children: grant.absolutePath }), (0, jsx_runtime_1.jsx)("td", { children: props.groupName }), (0, jsx_runtime_1.jsx)("td", { children: grant.packageId ?? "operator" }), (0, jsx_runtime_1.jsx)("td", { children: grant.mode }), (0, jsx_runtime_1.jsx)("td", { children: new Date(grant.createdAt).toLocaleString() }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-danger", disabled: props.busy, onClick: () => props.onRevoke(grant.grantId), children: "Revoke" }) })] }));
|
|
119
|
+
}
|
|
120
|
+
function AddFolderForm(props) {
|
|
121
|
+
return ((0, jsx_runtime_1.jsxs)("form", { className: "row g-2 align-items-end", onSubmit: (event) => {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
props.onAdd();
|
|
124
|
+
}, children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-5", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label", htmlFor: `local-folder-group-${props.fieldScope}`, children: "Group" }), (0, jsx_runtime_1.jsxs)("select", { id: `local-folder-group-${props.fieldScope}`, className: "form-select", value: props.groupId, onChange: (event) => props.onGroupId(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Select a group" }), props.groups.map((group) => ((0, jsx_runtime_1.jsx)("option", { value: group.groupId, children: group.name }, group.groupId)))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-3", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label", htmlFor: `local-folder-mode-${props.fieldScope}`, children: "Mode" }), (0, jsx_runtime_1.jsxs)("select", { id: `local-folder-mode-${props.fieldScope}`, className: "form-select", value: props.mode, onChange: (event) => props.onMode(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "readwrite", children: "readwrite" }), (0, jsx_runtime_1.jsx)("option", { value: "read", children: "read" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("button", { type: "submit", className: "btn btn-primary", disabled: props.busy || !props.groupId, children: props.busy ? "Working…" : "Add folder" }) })] }));
|
|
125
|
+
}
|
|
126
|
+
function isUnavailable(error) {
|
|
127
|
+
if (!error) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return /No contract provider registered|not available locally/i.test(error.message);
|
|
131
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type IContractConsumerBase, type IHostContractConsumer, type ILocalFolderGrantInfo, type ILocalFolderRequestGrantInput } from "@peers-app/peers-sdk";
|
|
2
|
+
/** Tools the Packages screens use on System Local Folders. */
|
|
3
|
+
export interface ILocalFoldersUiConsumer extends IContractConsumerBase {
|
|
4
|
+
tools: {
|
|
5
|
+
/** Create a grant. Omitting `absolutePath` opens the desktop folder picker. */
|
|
6
|
+
requestGrant(input: ILocalFolderRequestGrantInput): Promise<ILocalFolderGrantInfo>;
|
|
7
|
+
/** Active grants on this device. */
|
|
8
|
+
listGrants(): Promise<{
|
|
9
|
+
grants: ILocalFolderGrantInfo[];
|
|
10
|
+
}>;
|
|
11
|
+
/** Revoke one grant. `revoked` is false when it was already gone. */
|
|
12
|
+
revokeGrant(input: {
|
|
13
|
+
grantId: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
revoked: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Shared System Local Folders consumer for the Packages screens.
|
|
21
|
+
*
|
|
22
|
+
* The PWA host does not register this contract. Callers treat a missing provider as unavailable.
|
|
23
|
+
*/
|
|
24
|
+
export declare const localFoldersConsumer: IHostContractConsumer<ILocalFoldersUiConsumer>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.localFoldersConsumer = void 0;
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
/**
|
|
6
|
+
* Shared System Local Folders consumer for the Packages screens.
|
|
7
|
+
*
|
|
8
|
+
* The PWA host does not register this contract. Callers treat a missing provider as unavailable.
|
|
9
|
+
*/
|
|
10
|
+
exports.localFoldersConsumer = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getLocalFoldersContractDefinition)());
|
|
@@ -9,6 +9,7 @@ const save_button_1 = require("../../components/save-button");
|
|
|
9
9
|
const tabs_1 = require("../../components/tabs");
|
|
10
10
|
const hooks_1 = require("../../hooks");
|
|
11
11
|
const tabs_state_1 = require("../../tabs-layout/tabs-state");
|
|
12
|
+
const local_folder_grants_panel_1 = require("./local-folder-grants-panel");
|
|
12
13
|
const package_info_1 = require("./package-info");
|
|
13
14
|
const package_versions_1 = require("./package-versions");
|
|
14
15
|
/** Detail screen for a single package with Info, Versions, Components, and Dependencies tabs. */
|
|
@@ -75,6 +76,10 @@ const PackageDetails = (props) => {
|
|
|
75
76
|
name: "Dependencies",
|
|
76
77
|
content: ((0, jsx_runtime_1.jsx)(tabs_1.ScreenTabBody, { children: "TODO - show all of the dependencies of the package" })),
|
|
77
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "Folders",
|
|
81
|
+
content: ((0, jsx_runtime_1.jsx)(tabs_1.ScreenTabBody, { children: (0, jsx_runtime_1.jsx)(local_folder_grants_panel_1.LocalFolderGrantsPanel, { packageId: pkg.packageId }) })),
|
|
82
|
+
},
|
|
78
83
|
] }, pkg.packageId)] }));
|
|
79
84
|
};
|
|
80
85
|
exports.PackageDetails = PackageDetails;
|
|
@@ -11,6 +11,7 @@ const tooltip_1 = require("../../components/tooltip");
|
|
|
11
11
|
const globals_1 = require("../../globals");
|
|
12
12
|
const hooks_1 = require("../../hooks");
|
|
13
13
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
14
|
+
const local_folder_grants_panel_1 = require("./local-folder-grants-panel");
|
|
14
15
|
const package_helpers_1 = require("./package-helpers");
|
|
15
16
|
function tagBadgeClass(tag) {
|
|
16
17
|
if (tag === "dev")
|
|
@@ -120,6 +121,7 @@ const PackageRow = ({ pkg, onUpdated }) => {
|
|
|
120
121
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4 d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", style: { minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#packages/${pkg.packageId}`, children: pkg.name }), isDeviceOverridden && ((0, jsx_runtime_1.jsx)("span", { className: "text-warning ms-1", title: "This device is using a different version than the group", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-pc-display" }) })), status && ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", status.activePv.version] }), status.activePv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${tagBadgeClass(status.activePv.versionTag)}`, style: { fontSize: "0.65em" }, children: status.activePv.versionTag })), hasUpdate ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 ${updateBadgeClass(status.newerLevel)}`, style: { fontSize: "0.65em" }, children: ["v", status.newestPv?.version, " available"] })) : isOnDev && hasGroupRelease ? ((0, jsx_runtime_1.jsxs)("span", { className: "badge ms-1 text-bg-secondary", style: { fontSize: "0.65em" }, children: ["release: v", status.groupReleasePv?.version] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] })), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ["bottom", "top", "right", "left"] })] }), hasUpdate && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary ms-2 text-nowrap", disabled: updating, onClick: handleUpdate, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-up-circle me-1" })), "Update"] })), isOnDev && hasGroupRelease && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary ms-2 text-nowrap", disabled: updating, onClick: handleUseRelease, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" })), "Use Release"] })), hasDevVersion && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-danger ms-2 text-nowrap", disabled: updating, onClick: handleUseDev, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-tools me-1" })), "Use Dev"] }))] }));
|
|
121
122
|
};
|
|
122
123
|
/** Searchable list of installed packages, plus create/import from the search box. */
|
|
124
|
+
/** Packages screen: search, local folder grants, and the installed package list. */
|
|
123
125
|
const PackageList = () => {
|
|
124
126
|
const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
|
|
125
127
|
const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
|
|
@@ -243,7 +245,7 @@ const PackageList = () => {
|
|
|
243
245
|
if (addingPackage()) {
|
|
244
246
|
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "container-fluid d-flex justify-content-center mt-5", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-seam" }), "\u00A0 Adding Package"] }), (0, jsx_runtime_1.jsx)("div", { className: "container-fluid d-flex justify-content-center mt-2", children: (0, jsx_runtime_1.jsx)("div", { className: "progress", role: "progressbar", "aria-label": "Animated striped example", "aria-valuenow": 100, "aria-valuemin": 0, "aria-valuemax": 100, children: (0, jsx_runtime_1.jsx)("div", { className: "progress-bar progress-bar-striped progress-bar-animated", style: { width: 150 } }) }) })] }));
|
|
245
247
|
}
|
|
246
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => peers_sdk_1.rpcServerCalls.openPath(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: `${searchText}_${refreshKey()}`, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
|
|
248
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => peers_sdk_1.rpcServerCalls.openPath(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)(local_folder_grants_panel_1.LocalFolderGrantsPanel, {}), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: `${searchText}_${refreshKey()}`, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
|
|
247
249
|
return packages.map((pkg) => ((0, jsx_runtime_1.jsx)(PackageRow, { pkg: pkg, onUpdated: () => refreshKey(refreshKey() + 1) }, pkg.packageId)));
|
|
248
250
|
}, loadingIndicator: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 }, children: (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) }), endOfList: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 } }) }) })] }));
|
|
249
251
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.11",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "biome check --write ."
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@peers-app/peers-sdk": "^0.25.
|
|
31
|
+
"@peers-app/peers-sdk": "^0.25.11",
|
|
32
32
|
"bootstrap": "^5.3.3",
|
|
33
33
|
"react": "^18.0.0",
|
|
34
34
|
"react-dom": "^18.0.0"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@babel/preset-react": "^7.24.1",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.1",
|
|
41
41
|
"@biomejs/biome": "2.3.11",
|
|
42
|
-
"@peers-app/peers-sdk": "0.25.
|
|
42
|
+
"@peers-app/peers-sdk": "0.25.11",
|
|
43
43
|
"@testing-library/dom": "^10.4.0",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
45
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ILocalFolderGrantInfo, LocalFolderMode } from "@peers-app/peers-sdk";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { groups } from "../../globals";
|
|
4
|
+
import { useObservable } from "../../hooks";
|
|
5
|
+
import { useHostContract } from "../../hooks/use-host-contract";
|
|
6
|
+
import { localFoldersConsumer } from "./local-folders-ipc";
|
|
7
|
+
|
|
8
|
+
/** Props for {@link LocalFolderGrantsPanel}. */
|
|
9
|
+
export interface ILocalFolderGrantsPanelProps {
|
|
10
|
+
/**
|
|
11
|
+
* When set, the list and new grants are limited to this package.
|
|
12
|
+
* Omit it for the device-wide operator list.
|
|
13
|
+
*/
|
|
14
|
+
packageId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Folder grants on this device, with add and revoke.
|
|
19
|
+
*
|
|
20
|
+
* Add opens the host folder picker. A host that does not provide System Local Folders
|
|
21
|
+
* (the PWA) shows the contract as unavailable.
|
|
22
|
+
*/
|
|
23
|
+
export function LocalFolderGrantsPanel(props: ILocalFolderGrantsPanelProps) {
|
|
24
|
+
const { consumer, error: transportError } = useHostContract(localFoldersConsumer);
|
|
25
|
+
const [groupList] = useObservable(groups);
|
|
26
|
+
const [grants, setGrants] = useState<ILocalFolderGrantInfo[] | undefined>();
|
|
27
|
+
const [loadError, setLoadError] = useState<Error | undefined>();
|
|
28
|
+
const [actionError, setActionError] = useState<string | undefined>();
|
|
29
|
+
const [busy, setBusy] = useState(false);
|
|
30
|
+
const [groupId, setGroupId] = useState("");
|
|
31
|
+
const [mode, setMode] = useState<LocalFolderMode>("readwrite");
|
|
32
|
+
const [refresh, setRefresh] = useState(0);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!consumer) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
consumer.tools.listGrants().then(
|
|
40
|
+
(result) => {
|
|
41
|
+
if (!cancelled) {
|
|
42
|
+
setGrants(result.grants);
|
|
43
|
+
setLoadError(undefined);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
(cause: unknown) => {
|
|
47
|
+
if (!cancelled) {
|
|
48
|
+
setLoadError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
return () => {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
};
|
|
55
|
+
}, [consumer, refresh]);
|
|
56
|
+
|
|
57
|
+
const unavailable = isUnavailable(transportError) || isUnavailable(loadError);
|
|
58
|
+
const visible = (grants ?? []).filter((grant) =>
|
|
59
|
+
props.packageId ? grant.packageId === props.packageId : true,
|
|
60
|
+
);
|
|
61
|
+
const names = new Map(groupList.map((group) => [group.groupId, group.name || group.groupId]));
|
|
62
|
+
|
|
63
|
+
async function addFolder(): Promise<void> {
|
|
64
|
+
if (!consumer || !groupId) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setBusy(true);
|
|
68
|
+
setActionError(undefined);
|
|
69
|
+
try {
|
|
70
|
+
await consumer.tools.requestGrant({
|
|
71
|
+
groupId,
|
|
72
|
+
mode,
|
|
73
|
+
...(props.packageId ? { packageId: props.packageId } : {}),
|
|
74
|
+
});
|
|
75
|
+
setRefresh((value) => value + 1);
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
78
|
+
if (!/cancelled/i.test(message)) {
|
|
79
|
+
setActionError(message);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function revoke(grantId: string): Promise<void> {
|
|
87
|
+
if (!consumer) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!window.confirm("Revoke this folder grant?")) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setBusy(true);
|
|
94
|
+
setActionError(undefined);
|
|
95
|
+
try {
|
|
96
|
+
await consumer.tools.revokeGrant({ grantId });
|
|
97
|
+
setRefresh((value) => value + 1);
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
setActionError(cause instanceof Error ? cause.message : String(cause));
|
|
100
|
+
} finally {
|
|
101
|
+
setBusy(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<section className="mb-4">
|
|
107
|
+
<h5>{props.packageId ? "Folders" : "Local folders"}</h5>
|
|
108
|
+
<FolderStatus
|
|
109
|
+
unavailable={unavailable}
|
|
110
|
+
transportError={transportError}
|
|
111
|
+
loadError={loadError}
|
|
112
|
+
loading={!!consumer && grants === undefined && !loadError}
|
|
113
|
+
/>
|
|
114
|
+
{consumer && !unavailable && (
|
|
115
|
+
<>
|
|
116
|
+
<GrantTable grants={visible} names={names} busy={busy} onRevoke={revoke} />
|
|
117
|
+
<AddFolderForm
|
|
118
|
+
fieldScope={props.packageId ?? "device"}
|
|
119
|
+
groupId={groupId}
|
|
120
|
+
groups={groupList.map((group) => ({
|
|
121
|
+
groupId: group.groupId,
|
|
122
|
+
name: group.name || group.groupId,
|
|
123
|
+
}))}
|
|
124
|
+
mode={mode}
|
|
125
|
+
busy={busy}
|
|
126
|
+
onGroupId={setGroupId}
|
|
127
|
+
onMode={setMode}
|
|
128
|
+
onAdd={addFolder}
|
|
129
|
+
/>
|
|
130
|
+
{actionError && <div className="alert alert-danger mt-2 mb-0">{actionError}</div>}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</section>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function FolderStatus(props: {
|
|
138
|
+
unavailable: boolean;
|
|
139
|
+
transportError: Error | undefined;
|
|
140
|
+
loadError: Error | undefined;
|
|
141
|
+
loading: boolean;
|
|
142
|
+
}) {
|
|
143
|
+
if (props.unavailable) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="alert alert-secondary mb-0">
|
|
146
|
+
System Local Folders is not available on this host.
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const error = props.loadError ?? props.transportError;
|
|
151
|
+
if (error) {
|
|
152
|
+
return <div className="alert alert-danger mb-0">{error.message}</div>;
|
|
153
|
+
}
|
|
154
|
+
if (props.loading) {
|
|
155
|
+
return <div className="text-muted">Loading folder grants…</div>;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function GrantTable(props: {
|
|
161
|
+
grants: ILocalFolderGrantInfo[];
|
|
162
|
+
names: Map<string, string>;
|
|
163
|
+
busy: boolean;
|
|
164
|
+
onRevoke: (grantId: string) => void;
|
|
165
|
+
}) {
|
|
166
|
+
if (props.grants.length === 0) {
|
|
167
|
+
return <p className="text-muted">No folder grants.</p>;
|
|
168
|
+
}
|
|
169
|
+
return (
|
|
170
|
+
<div className="table-responsive">
|
|
171
|
+
<table className="table table-sm align-middle">
|
|
172
|
+
<thead>
|
|
173
|
+
<tr>
|
|
174
|
+
<th>Path</th>
|
|
175
|
+
<th>Group</th>
|
|
176
|
+
<th>Package</th>
|
|
177
|
+
<th>Mode</th>
|
|
178
|
+
<th>Created</th>
|
|
179
|
+
<th />
|
|
180
|
+
</tr>
|
|
181
|
+
</thead>
|
|
182
|
+
<tbody>
|
|
183
|
+
{props.grants.map((grant) => (
|
|
184
|
+
<GrantRow
|
|
185
|
+
key={grant.grantId}
|
|
186
|
+
grant={grant}
|
|
187
|
+
groupName={props.names.get(grant.groupId) ?? grant.groupId}
|
|
188
|
+
busy={props.busy}
|
|
189
|
+
onRevoke={props.onRevoke}
|
|
190
|
+
/>
|
|
191
|
+
))}
|
|
192
|
+
</tbody>
|
|
193
|
+
</table>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function GrantRow(props: {
|
|
199
|
+
grant: ILocalFolderGrantInfo;
|
|
200
|
+
groupName: string;
|
|
201
|
+
busy: boolean;
|
|
202
|
+
onRevoke: (grantId: string) => void;
|
|
203
|
+
}) {
|
|
204
|
+
const { grant } = props;
|
|
205
|
+
return (
|
|
206
|
+
<tr>
|
|
207
|
+
<td className="text-break">{grant.absolutePath}</td>
|
|
208
|
+
<td>{props.groupName}</td>
|
|
209
|
+
<td>{grant.packageId ?? "operator"}</td>
|
|
210
|
+
<td>{grant.mode}</td>
|
|
211
|
+
<td>{new Date(grant.createdAt).toLocaleString()}</td>
|
|
212
|
+
<td>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className="btn btn-sm btn-outline-danger"
|
|
216
|
+
disabled={props.busy}
|
|
217
|
+
onClick={() => props.onRevoke(grant.grantId)}
|
|
218
|
+
>
|
|
219
|
+
Revoke
|
|
220
|
+
</button>
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function AddFolderForm(props: {
|
|
227
|
+
fieldScope: string;
|
|
228
|
+
groupId: string;
|
|
229
|
+
groups: { groupId: string; name: string }[];
|
|
230
|
+
mode: LocalFolderMode;
|
|
231
|
+
busy: boolean;
|
|
232
|
+
onGroupId: (groupId: string) => void;
|
|
233
|
+
onMode: (mode: LocalFolderMode) => void;
|
|
234
|
+
onAdd: () => void;
|
|
235
|
+
}) {
|
|
236
|
+
return (
|
|
237
|
+
<form
|
|
238
|
+
className="row g-2 align-items-end"
|
|
239
|
+
onSubmit={(event) => {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
props.onAdd();
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<div className="col-md-5">
|
|
245
|
+
<label className="form-label" htmlFor={`local-folder-group-${props.fieldScope}`}>
|
|
246
|
+
Group
|
|
247
|
+
</label>
|
|
248
|
+
<select
|
|
249
|
+
id={`local-folder-group-${props.fieldScope}`}
|
|
250
|
+
className="form-select"
|
|
251
|
+
value={props.groupId}
|
|
252
|
+
onChange={(event) => props.onGroupId(event.target.value)}
|
|
253
|
+
>
|
|
254
|
+
<option value="">Select a group</option>
|
|
255
|
+
{props.groups.map((group) => (
|
|
256
|
+
<option key={group.groupId} value={group.groupId}>
|
|
257
|
+
{group.name}
|
|
258
|
+
</option>
|
|
259
|
+
))}
|
|
260
|
+
</select>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="col-md-3">
|
|
263
|
+
<label className="form-label" htmlFor={`local-folder-mode-${props.fieldScope}`}>
|
|
264
|
+
Mode
|
|
265
|
+
</label>
|
|
266
|
+
<select
|
|
267
|
+
id={`local-folder-mode-${props.fieldScope}`}
|
|
268
|
+
className="form-select"
|
|
269
|
+
value={props.mode}
|
|
270
|
+
onChange={(event) => props.onMode(event.target.value as LocalFolderMode)}
|
|
271
|
+
>
|
|
272
|
+
<option value="readwrite">readwrite</option>
|
|
273
|
+
<option value="read">read</option>
|
|
274
|
+
</select>
|
|
275
|
+
</div>
|
|
276
|
+
<div className="col-md-4">
|
|
277
|
+
<button type="submit" className="btn btn-primary" disabled={props.busy || !props.groupId}>
|
|
278
|
+
{props.busy ? "Working…" : "Add folder"}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</form>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isUnavailable(error: Error | undefined): boolean {
|
|
286
|
+
if (!error) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return /No contract provider registered|not available locally/i.test(error.message);
|
|
290
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLocalFoldersContractDefinition,
|
|
3
|
+
hostContractConsumer,
|
|
4
|
+
type IContractConsumerBase,
|
|
5
|
+
type IHostContractConsumer,
|
|
6
|
+
type ILocalFolderGrantInfo,
|
|
7
|
+
type ILocalFolderRequestGrantInput,
|
|
8
|
+
} from "@peers-app/peers-sdk";
|
|
9
|
+
|
|
10
|
+
/** Tools the Packages screens use on System Local Folders. */
|
|
11
|
+
export interface ILocalFoldersUiConsumer extends IContractConsumerBase {
|
|
12
|
+
tools: {
|
|
13
|
+
/** Create a grant. Omitting `absolutePath` opens the desktop folder picker. */
|
|
14
|
+
requestGrant(input: ILocalFolderRequestGrantInput): Promise<ILocalFolderGrantInfo>;
|
|
15
|
+
/** Active grants on this device. */
|
|
16
|
+
listGrants(): Promise<{ grants: ILocalFolderGrantInfo[] }>;
|
|
17
|
+
/** Revoke one grant. `revoked` is false when it was already gone. */
|
|
18
|
+
revokeGrant(input: { grantId: string }): Promise<{ revoked: boolean }>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shared System Local Folders consumer for the Packages screens.
|
|
24
|
+
*
|
|
25
|
+
* The PWA host does not register this contract. Callers treat a missing provider as unavailable.
|
|
26
|
+
*/
|
|
27
|
+
export const localFoldersConsumer: IHostContractConsumer<ILocalFoldersUiConsumer> =
|
|
28
|
+
hostContractConsumer<ILocalFoldersUiConsumer>(getLocalFoldersContractDefinition());
|
|
@@ -5,6 +5,7 @@ import { type ISaveButtonProps, SaveButton } from "../../components/save-button"
|
|
|
5
5
|
import { ScreenTabBody, Tabs } from "../../components/tabs";
|
|
6
6
|
import { useObservableState, usePromise } from "../../hooks";
|
|
7
7
|
import { updateActiveTabTitle } from "../../tabs-layout/tabs-state";
|
|
8
|
+
import { LocalFolderGrantsPanel } from "./local-folder-grants-panel";
|
|
8
9
|
import { PackageInfo } from "./package-info";
|
|
9
10
|
import { PackageVersionsList } from "./package-versions";
|
|
10
11
|
|
|
@@ -127,6 +128,14 @@ export const PackageDetails = (props: IProps) => {
|
|
|
127
128
|
<ScreenTabBody>TODO - show all of the dependencies of the package</ScreenTabBody>
|
|
128
129
|
),
|
|
129
130
|
},
|
|
131
|
+
{
|
|
132
|
+
name: "Folders",
|
|
133
|
+
content: (
|
|
134
|
+
<ScreenTabBody>
|
|
135
|
+
<LocalFolderGrantsPanel packageId={pkg.packageId} />
|
|
136
|
+
</ScreenTabBody>
|
|
137
|
+
),
|
|
138
|
+
},
|
|
130
139
|
]}
|
|
131
140
|
/>
|
|
132
141
|
</div>
|
|
@@ -21,6 +21,7 @@ import { Tooltip } from "../../components/tooltip";
|
|
|
21
21
|
import { isDesktop, mainContentPath } from "../../globals";
|
|
22
22
|
import { useObservable, useObservableState, usePromise } from "../../hooks";
|
|
23
23
|
import { registerInternalPeersUI } from "../../ui-router/ui-loader";
|
|
24
|
+
import { LocalFolderGrantsPanel } from "./local-folder-grants-panel";
|
|
24
25
|
import {
|
|
25
26
|
activatePackageVersion,
|
|
26
27
|
checkVersionStatus,
|
|
@@ -236,6 +237,7 @@ const PackageRow = ({ pkg, onUpdated }: { pkg: IPackage; onUpdated: () => void }
|
|
|
236
237
|
};
|
|
237
238
|
|
|
238
239
|
/** Searchable list of installed packages, plus create/import from the search box. */
|
|
240
|
+
/** Packages screen: search, local folder grants, and the installed package list. */
|
|
239
241
|
export const PackageList = () => {
|
|
240
242
|
const [searchTextObs] = useState(() => observable(""));
|
|
241
243
|
const [searchText] = useObservable(searchTextObs);
|
|
@@ -437,6 +439,8 @@ export const PackageList = () => {
|
|
|
437
439
|
</ul>
|
|
438
440
|
</div>
|
|
439
441
|
|
|
442
|
+
<LocalFolderGrantsPanel />
|
|
443
|
+
|
|
440
444
|
<div className="peers-list-container">
|
|
441
445
|
<LazyList
|
|
442
446
|
resetTrigger={`${searchText}_${refreshKey()}`}
|