@metricinsights/pp-dev 1.5.0-beta.3 → 1.5.0-beta.31
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 +200 -145
- package/dist/cjs/auth.provider-fI0tFsf5.js +2 -0
- package/dist/cjs/auth.provider-fI0tFsf5.js.map +1 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/index-Ty1d2YCg.js +2 -0
- package/dist/cjs/index-Ty1d2YCg.js.map +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/migrate-Gi9k9cLw.js +2 -0
- package/dist/cjs/migrate-Gi9k9cLw.js.map +1 -0
- package/dist/cjs/package.json +2 -135
- package/dist/cjs/plugin.js +1 -1
- package/dist/cjs/plugin.js.map +1 -1
- package/dist/cjs/request-capture.middleware-6DhFLyIz.js +2 -0
- package/dist/cjs/request-capture.middleware-6DhFLyIz.js.map +1 -0
- package/dist/cjs/version-manifest-CVBuIKBS.js +2 -0
- package/dist/cjs/version-manifest-CVBuIKBS.js.map +1 -0
- package/dist/cjs/version-plugin-DBdqn2Rn.js +2 -0
- package/dist/cjs/version-plugin-DBdqn2Rn.js.map +1 -0
- package/dist/client/client.js +278 -126
- package/dist/client/client.js.map +1 -1
- package/dist/client/request-inspector.css +3 -0
- package/dist/client/request-inspector.css.map +1 -0
- package/dist/client/request-inspector.html +87 -0
- package/dist/client/request-inspector.js +549 -0
- package/dist/client/request-inspector.js.map +1 -0
- package/dist/client/variables-editor.css +3 -0
- package/dist/client/variables-editor.css.map +1 -0
- package/dist/client/variables-editor.html +44 -0
- package/dist/client/variables-editor.js +3474 -0
- package/dist/client/variables-editor.js.map +1 -0
- package/dist/esm/auth.provider-DDUsu1H1.js +2 -0
- package/dist/esm/auth.provider-DDUsu1H1.js.map +1 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index-tD-QICGk.js +2 -0
- package/dist/esm/index-tD-QICGk.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/migrate-CCBRuVfj.js +2 -0
- package/dist/esm/{migrate-B2gJ62fm.js.map → migrate-CCBRuVfj.js.map} +1 -1
- package/dist/esm/package.json +2 -135
- package/dist/esm/plugin.js +1 -1
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/request-capture.middleware-VElwjU1W.js +2 -0
- package/dist/esm/request-capture.middleware-VElwjU1W.js.map +1 -0
- package/dist/esm/version-manifest-CyzmnFOe.js +2 -0
- package/dist/esm/version-manifest-CyzmnFOe.js.map +1 -0
- package/dist/esm/version-plugin-6rxIgQ-I.js +2 -0
- package/dist/esm/version-plugin-6rxIgQ-I.js.map +1 -0
- package/dist/types/helpers.d.ts +29 -0
- package/dist/types/index.d.ts +15 -156
- package/dist/types/plugin.d.ts +8 -0
- package/dist/types/pp-dev-config-D889A-PO.d.ts +189 -0
- package/docs/features.md +2 -1
- package/package.json +26 -35
- package/dist/CHANGELOG.md +0 -442
- package/dist/README.md +0 -724
- package/dist/cjs/index-CCUrjlJE.js +0 -2
- package/dist/cjs/index-CCUrjlJE.js.map +0 -1
- package/dist/cjs/migrate-BYno__sx.js +0 -2
- package/dist/cjs/migrate-BYno__sx.js.map +0 -1
- package/dist/cjs/plugin-D_RXI7ru.js +0 -2
- package/dist/cjs/plugin-D_RXI7ru.js.map +0 -1
- package/dist/cjs/version-plugin-Bf4jLh17.js +0 -2
- package/dist/cjs/version-plugin-Bf4jLh17.js.map +0 -1
- package/dist/esm/index-BazQY12s.js +0 -2
- package/dist/esm/index-BazQY12s.js.map +0 -1
- package/dist/esm/migrate-B2gJ62fm.js +0 -2
- package/dist/esm/plugin-NIWxLreC.js +0 -2
- package/dist/esm/plugin-NIWxLreC.js.map +0 -1
- package/dist/esm/version-plugin-DwuZCHob.js +0 -2
- package/dist/esm/version-plugin-DwuZCHob.js.map +0 -1
package/dist/client/client.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
* custom-event wire shape Vite uses: `{ type: 'custom', event, data }`.
|
|
6
|
-
*
|
|
7
|
-
* Used only as a fallback: when `import.meta.hot` exists (Vite), that is used
|
|
8
|
-
* instead and this code never runs.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* WebSocket path the pp-dev Next.js server listens on for dev-panel messages.
|
|
12
|
-
* Kept in sync with `PP_DEV_HMR_WS_PATH` in `src/constants.ts` (server side).
|
|
2
|
+
* The WebSocket messages between the dev server and the dev panel, for both sides: the server
|
|
3
|
+
* (`src/lib/client.service.ts`, `src/lib/pp-ws-server.ts`) and the panel (`src/client/**`) import
|
|
4
|
+
* them from here, so they can't drift apart. No Node or DOM imports: the panel bundle includes it.
|
|
13
5
|
*/
|
|
6
|
+
/** WebSocket path the `pp-dev next` server listens on for dev-panel messages. */
|
|
14
7
|
const PP_DEV_HMR_WS_PATH = '/@pp-dev-hmr';
|
|
8
|
+
/** `template:sync:response` code for a sync requested while another one runs. */
|
|
9
|
+
const SYNC_IN_PROGRESS_CODE = 'SYNC_IN_PROGRESS';
|
|
10
|
+
|
|
15
11
|
const MAX_OUTBOX_SIZE = 50;
|
|
16
12
|
function resolveWebSocketUrl() {
|
|
17
13
|
const { protocol, host } = window.location;
|
|
@@ -23,6 +19,8 @@ function createPPDevHotContext() {
|
|
|
23
19
|
/** Outgoing messages queued while the socket is not OPEN; flushed on connect. */
|
|
24
20
|
const outbox = [];
|
|
25
21
|
let socket = null;
|
|
22
|
+
/** The connection state last reported to the panel, so each change is reported once. */
|
|
23
|
+
let reported = null;
|
|
26
24
|
let reconnectTimer = null;
|
|
27
25
|
let reconnectDelay = 1_000;
|
|
28
26
|
const MAX_RECONNECT_DELAY = 10_000;
|
|
@@ -60,6 +58,8 @@ function createPPDevHotContext() {
|
|
|
60
58
|
}
|
|
61
59
|
socket.addEventListener('open', () => {
|
|
62
60
|
reconnectDelay = 1_000;
|
|
61
|
+
reported = 'connected';
|
|
62
|
+
dispatch('vite:ws:connect', {});
|
|
63
63
|
flushOutbox();
|
|
64
64
|
});
|
|
65
65
|
socket.addEventListener('message', (ev) => {
|
|
@@ -76,6 +76,14 @@ function createPPDevHotContext() {
|
|
|
76
76
|
});
|
|
77
77
|
socket.addEventListener('close', () => {
|
|
78
78
|
socket = null;
|
|
79
|
+
// Same events as Vite's own HMR client, so the panel can tell a lost connection apart
|
|
80
|
+
// (e.g. a sync whose response will never arrive, or a click that can't be sent). Once per
|
|
81
|
+
// loss: each failed reconnect attempt is not a new one, but a first connection that never
|
|
82
|
+
// opened is.
|
|
83
|
+
if (reported !== 'disconnected') {
|
|
84
|
+
reported = 'disconnected';
|
|
85
|
+
dispatch('vite:ws:disconnect', {});
|
|
86
|
+
}
|
|
79
87
|
scheduleReconnect();
|
|
80
88
|
});
|
|
81
89
|
socket.addEventListener('error', () => {
|
|
@@ -119,6 +127,9 @@ function createPPDevHotContext() {
|
|
|
119
127
|
outbox.push(payload);
|
|
120
128
|
}
|
|
121
129
|
},
|
|
130
|
+
// Not part of Vite's API: lets the panel tell an open connection from one still connecting, whose
|
|
131
|
+
// queued messages would go out whenever it opens (see `src/client/sync-button.ts`).
|
|
132
|
+
isConnected: () => socket?.readyState === WebSocket.OPEN,
|
|
122
133
|
// Unused by the dev-panel client; provided as no-ops to satisfy the shape.
|
|
123
134
|
accept: noop,
|
|
124
135
|
acceptExports: noop,
|
|
@@ -644,6 +655,37 @@ function initPanelSettings($panel, controller, hooks) {
|
|
|
644
655
|
});
|
|
645
656
|
}
|
|
646
657
|
|
|
658
|
+
/**
|
|
659
|
+
* The rules for a saved user's name, which is also its host (`<name>.localhost`), for both sides:
|
|
660
|
+
* the server checks them (`src/lib/user-registry.ts`) and the dev panel's Users popover
|
|
661
|
+
* (`src/client/users-panel.ts`) checks them as you type. No Node or DOM imports: the panel bundle
|
|
662
|
+
* includes it.
|
|
663
|
+
*/
|
|
664
|
+
/** Longest user name; well under the 63 characters a DNS label allows. */
|
|
665
|
+
const MAX_USER_NAME_LENGTH = 32;
|
|
666
|
+
/**
|
|
667
|
+
* A DNS label: lowercase latin letters, digits and `-`, starting with a letter and not ending with
|
|
668
|
+
* `-`. Also used as an input's `pattern`, which browsers compile with the `v` flag: there an
|
|
669
|
+
* unescaped `-` in a class is a syntax error, and the whole pattern was silently ignored.
|
|
670
|
+
*/
|
|
671
|
+
const USER_NAME_PATTERN = String.raw `[a-z](?:[a-z0-9\-]*[a-z0-9])?`;
|
|
672
|
+
const USER_NAME_REGEXP = new RegExp(`^(?:${USER_NAME_PATTERN})$`);
|
|
673
|
+
/** The suffix of a user's host: `viewer` works on `viewer.localhost`. */
|
|
674
|
+
const USER_HOST_SUFFIX = '.localhost';
|
|
675
|
+
/** Why `name` can't be a user name, or `undefined` if it can. */
|
|
676
|
+
function getUserNameError(name) {
|
|
677
|
+
if (!name) {
|
|
678
|
+
return 'The name is required';
|
|
679
|
+
}
|
|
680
|
+
if (name.length > MAX_USER_NAME_LENGTH) {
|
|
681
|
+
return `The name must be at most ${MAX_USER_NAME_LENGTH} characters long`;
|
|
682
|
+
}
|
|
683
|
+
if (!USER_NAME_REGEXP.test(name)) {
|
|
684
|
+
return 'The name may contain only lowercase latin letters, digits and "-", must start with a letter and must not end with "-"';
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
647
689
|
/**
|
|
648
690
|
* The dev panel's "Users" popover (PP-4597): the user of this tab, the saved users (open as, log
|
|
649
691
|
* in, make default, remove) and adding a user by API token or by username + password. Each user
|
|
@@ -651,7 +693,8 @@ function initPanelSettings($panel, controller, hooks) {
|
|
|
651
693
|
*/
|
|
652
694
|
const USERS_API = '/@api/users';
|
|
653
695
|
const REFRESH_INTERVAL = 3000;
|
|
654
|
-
|
|
696
|
+
// The server's name rules (src/shared/user-names.ts), not a copy of them.
|
|
697
|
+
const USER_HOST_REGEXP = new RegExp(`^(${USER_NAME_PATTERN})${USER_HOST_SUFFIX.replace('.', '\\.')}$`);
|
|
655
698
|
/** Marks the user this tab works as, apart from the radio, which picks localhost's user. */
|
|
656
699
|
const THIS_TAB_BADGE = '<span class="pp-dev-info__users-badge">this tab</span>';
|
|
657
700
|
const STATE_LABELS = {
|
|
@@ -716,6 +759,9 @@ function initUsersPanel($panel, hooks) {
|
|
|
716
759
|
let data = null;
|
|
717
760
|
// What the list shows now, so a refresh with the same data leaves it (and the focus) alone.
|
|
718
761
|
let renderedData = '';
|
|
762
|
+
// Only the latest refresh paints: two quick default switches sent two, and the first one's
|
|
763
|
+
// answer, arriving last, drew the state before the second.
|
|
764
|
+
let refreshSeq = 0;
|
|
719
765
|
// The token user being edited, whose row shows the edit form instead.
|
|
720
766
|
let editing = null;
|
|
721
767
|
const onDocClick = (ev) => {
|
|
@@ -823,7 +869,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
823
869
|
const name = escapeHtml(user.name);
|
|
824
870
|
if (user.kind === 'token' && user.name === editing) {
|
|
825
871
|
return `<form class="pp-dev-info__users-row pp-dev-info__users-edit" data-user="${name}">
|
|
826
|
-
<input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="
|
|
872
|
+
<input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="${MAX_USER_NAME_LENGTH}" pattern="${USER_NAME_PATTERN}"
|
|
827
873
|
autocomplete="off" spellcheck="false" aria-label="Name" data-focus-key="${name}:edit-name"
|
|
828
874
|
title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
|
|
829
875
|
<input class="pp-dev-info__users-input pp-dev-info__users-edit-token" type="password" autocomplete="off"
|
|
@@ -868,6 +914,9 @@ function initUsersPanel($panel, hooks) {
|
|
|
868
914
|
$open.addEventListener('click', (ev) => {
|
|
869
915
|
ev.preventDefault();
|
|
870
916
|
const target = $open.dataset.open;
|
|
917
|
+
if (!target) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
871
920
|
window.open($open.dataset.raw ? target : samePageOn(target), '_blank');
|
|
872
921
|
});
|
|
873
922
|
});
|
|
@@ -880,7 +929,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
880
929
|
$list.querySelectorAll('[data-edit]').forEach(($edit) => {
|
|
881
930
|
$edit.addEventListener('click', (ev) => {
|
|
882
931
|
ev.preventDefault();
|
|
883
|
-
startEditing($edit.dataset.edit);
|
|
932
|
+
startEditing($edit.dataset.edit ?? null);
|
|
884
933
|
});
|
|
885
934
|
});
|
|
886
935
|
$list.querySelector('[data-cancel-edit]')?.addEventListener('click', (ev) => {
|
|
@@ -892,8 +941,11 @@ function initUsersPanel($panel, hooks) {
|
|
|
892
941
|
ev.preventDefault();
|
|
893
942
|
const name = editing;
|
|
894
943
|
const $form = ev.currentTarget;
|
|
895
|
-
|
|
896
|
-
|
|
944
|
+
if (!name) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const newName = $form.querySelector('.pp-dev-info__users-edit-name')?.value.trim() ?? '';
|
|
948
|
+
const token = $form.querySelector('.pp-dev-info__users-edit-token')?.value.trim() ?? '';
|
|
897
949
|
const response = await request('PATCH', `${USERS_API}/${encodeURIComponent(name)}`, {
|
|
898
950
|
name: newName,
|
|
899
951
|
...(token ? { token } : {}),
|
|
@@ -914,7 +966,8 @@ function initUsersPanel($panel, hooks) {
|
|
|
914
966
|
$list.querySelectorAll('[data-remove]').forEach(($remove) => {
|
|
915
967
|
$remove.addEventListener('click', async (ev) => {
|
|
916
968
|
ev.preventDefault();
|
|
917
|
-
|
|
969
|
+
const name = $remove.dataset.remove;
|
|
970
|
+
if (name && (await request('DELETE', `${USERS_API}/${encodeURIComponent(name)}`))) {
|
|
918
971
|
await refresh();
|
|
919
972
|
}
|
|
920
973
|
});
|
|
@@ -950,12 +1003,17 @@ function initUsersPanel($panel, hooks) {
|
|
|
950
1003
|
}
|
|
951
1004
|
};
|
|
952
1005
|
const refresh = async () => {
|
|
1006
|
+
const seq = ++refreshSeq;
|
|
953
1007
|
try {
|
|
954
1008
|
const response = await fetch(USERS_API);
|
|
955
1009
|
if (!response.ok) {
|
|
956
1010
|
return;
|
|
957
1011
|
}
|
|
958
|
-
|
|
1012
|
+
const loaded = (await response.json());
|
|
1013
|
+
if (seq !== refreshSeq) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
data = loaded;
|
|
959
1017
|
renderList();
|
|
960
1018
|
renderAvailability();
|
|
961
1019
|
}
|
|
@@ -995,20 +1053,34 @@ function initUsersPanel($panel, hooks) {
|
|
|
995
1053
|
const $name = $popover.querySelector('.pp-dev-info__users-name-input');
|
|
996
1054
|
const $token = $popover.querySelector('.pp-dev-info__users-token');
|
|
997
1055
|
const $submit = $popover.querySelector('.pp-dev-info__users-submit');
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1056
|
+
if (!$name || !$token || !$submit) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const name = $name.value.trim();
|
|
1060
|
+
const nameError = getUserNameError(name);
|
|
1061
|
+
if (nameError) {
|
|
1062
|
+
setError(nameError);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const body = addKind === 'token' ? { name, token: $token.value } : { name, kind: 'session' };
|
|
1066
|
+
// A session user logs in on a tab of its own. Opened here, while the click still counts as the
|
|
1067
|
+
// user's: opened after the request, Safari and Chrome blocked it as a popup.
|
|
1068
|
+
const loginTab = addKind === 'session' ? window.open('', '_blank') : null;
|
|
1001
1069
|
$submit.disabled = true;
|
|
1002
1070
|
const response = await request('POST', USERS_API, body);
|
|
1003
1071
|
$submit.disabled = false;
|
|
1004
1072
|
if (!response) {
|
|
1073
|
+
loginTab?.close();
|
|
1005
1074
|
return;
|
|
1006
1075
|
}
|
|
1007
1076
|
const { user } = (await response.json());
|
|
1008
1077
|
$name.value = '';
|
|
1009
1078
|
$token.value = '';
|
|
1010
|
-
if (user.kind === 'session' && user.loginUrl) {
|
|
1011
|
-
|
|
1079
|
+
if (user.kind === 'session' && user.loginUrl && loginTab) {
|
|
1080
|
+
loginTab.location.href = user.loginUrl;
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
loginTab?.close();
|
|
1012
1084
|
}
|
|
1013
1085
|
await refresh();
|
|
1014
1086
|
};
|
|
@@ -1033,8 +1105,8 @@ function initUsersPanel($panel, hooks) {
|
|
|
1033
1105
|
<button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="token">API token</button>
|
|
1034
1106
|
<button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="session">Username + password</button>
|
|
1035
1107
|
</div>
|
|
1036
|
-
<input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="
|
|
1037
|
-
autocomplete="off" spellcheck="false" pattern="
|
|
1108
|
+
<input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="${MAX_USER_NAME_LENGTH}"
|
|
1109
|
+
autocomplete="off" spellcheck="false" pattern="${USER_NAME_PATTERN}"
|
|
1038
1110
|
title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
|
|
1039
1111
|
<input class="pp-dev-info__users-input pp-dev-info__users-token" type="password" placeholder="API token" autocomplete="off" />
|
|
1040
1112
|
<button type="submit" class="pp-dev-info__vars-btn pp-dev-info__users-submit">Add user</button>
|
|
@@ -1050,7 +1122,7 @@ function initUsersPanel($panel, hooks) {
|
|
|
1050
1122
|
renderKind();
|
|
1051
1123
|
});
|
|
1052
1124
|
});
|
|
1053
|
-
$popover.querySelector('.pp-dev-info__users-add')
|
|
1125
|
+
$popover.querySelector('.pp-dev-info__users-add')?.addEventListener('submit', (ev) => {
|
|
1054
1126
|
ev.preventDefault();
|
|
1055
1127
|
submit();
|
|
1056
1128
|
});
|
|
@@ -1139,6 +1211,175 @@ function createPopupElement(opts) {
|
|
|
1139
1211
|
$popup.appendChild($popupContent);
|
|
1140
1212
|
return $popup;
|
|
1141
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Calls `onElapsed` once the page has been visible for `duration` ms in all (time in a hidden tab
|
|
1216
|
+
* doesn't count). Returns a function that stops waiting, which also removes the page's
|
|
1217
|
+
* `visibilitychange` listener: one used to stay behind for every popup ever shown.
|
|
1218
|
+
*/
|
|
1219
|
+
function startAutoDismiss(duration, onElapsed) {
|
|
1220
|
+
let remaining = duration;
|
|
1221
|
+
let lastUpdate = Date.now();
|
|
1222
|
+
let visible = true;
|
|
1223
|
+
let frame;
|
|
1224
|
+
const tick = () => {
|
|
1225
|
+
frame = undefined;
|
|
1226
|
+
if (!visible) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const now = Date.now();
|
|
1230
|
+
remaining -= now - lastUpdate;
|
|
1231
|
+
lastUpdate = now;
|
|
1232
|
+
if (remaining <= 0) {
|
|
1233
|
+
stop();
|
|
1234
|
+
onElapsed();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
frame = requestAnimationFrame(tick);
|
|
1238
|
+
};
|
|
1239
|
+
const onVisibilityChange = () => {
|
|
1240
|
+
visible = !document.hidden;
|
|
1241
|
+
if (visible) {
|
|
1242
|
+
lastUpdate = Date.now();
|
|
1243
|
+
frame ??= requestAnimationFrame(tick);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
const stop = () => {
|
|
1247
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
1248
|
+
if (frame !== undefined) {
|
|
1249
|
+
cancelAnimationFrame(frame);
|
|
1250
|
+
frame = undefined;
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
1254
|
+
frame = requestAnimationFrame(tick);
|
|
1255
|
+
return stop;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const TITLE_AVAILABLE = 'Sync template';
|
|
1259
|
+
const TITLE_UNAVAILABLE = 'Sync is unavailable on this instance';
|
|
1260
|
+
/**
|
|
1261
|
+
* Wires the dev panel's "Sync template" button to the dev server.
|
|
1262
|
+
*
|
|
1263
|
+
* The button is disabled while a sync runs, so a double click can't start a second build, and is
|
|
1264
|
+
* enabled again by whatever ends the sync: its response, or a lost connection. It stays disabled
|
|
1265
|
+
* only when the server says sync is unavailable on this instance (`config.canSync === false`);
|
|
1266
|
+
* a failed sync (MI unreachable, a build error) can simply be retried.
|
|
1267
|
+
*/
|
|
1268
|
+
function initSyncButton(hot, $button, deps) {
|
|
1269
|
+
const reload = deps.reload ?? (() => window.location.reload());
|
|
1270
|
+
let available = !$button.disabled && !$button.classList.contains('disabled');
|
|
1271
|
+
let syncing = false;
|
|
1272
|
+
// Vite's connection is taken as open until reported lost: it may have opened before these handlers
|
|
1273
|
+
// were registered, and its client reloads the page once a lost one is back. The `pp-dev next`
|
|
1274
|
+
// transport says itself whether its socket is open, also while the first one is still connecting.
|
|
1275
|
+
let connected = true;
|
|
1276
|
+
const isConnected = () => connected && (hot.isConnected?.() ?? true);
|
|
1277
|
+
const applyState = () => {
|
|
1278
|
+
$button.disabled = syncing || !available;
|
|
1279
|
+
$button.classList.toggle('syncing', syncing);
|
|
1280
|
+
$button.classList.toggle('disabled', !available);
|
|
1281
|
+
$button.title = available ? TITLE_AVAILABLE : TITLE_UNAVAILABLE;
|
|
1282
|
+
if (syncing) {
|
|
1283
|
+
$button.setAttribute('aria-busy', 'true');
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
$button.removeAttribute('aria-busy');
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const endSync = () => {
|
|
1290
|
+
syncing = false;
|
|
1291
|
+
applyState();
|
|
1292
|
+
};
|
|
1293
|
+
hot.on('client:config:update', (data) => {
|
|
1294
|
+
if (typeof data?.config?.canSync !== 'boolean') {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
// The server sends `canSync: true` at the start of every sync: it must not enable the
|
|
1298
|
+
// button while that sync runs, which `applyState` takes care of.
|
|
1299
|
+
available = data.config.canSync;
|
|
1300
|
+
applyState();
|
|
1301
|
+
});
|
|
1302
|
+
hot.on('template:sync:action-required', async (payload) => {
|
|
1303
|
+
// The spinner keeps running while a confirmation modal is shown: the sync is still in
|
|
1304
|
+
// progress and only ends on `template:sync:response`.
|
|
1305
|
+
const approved = await deps.confirmModal({
|
|
1306
|
+
title: payload.title,
|
|
1307
|
+
content: payload.content,
|
|
1308
|
+
confirmText: payload.confirmText,
|
|
1309
|
+
cancelText: payload.cancelText,
|
|
1310
|
+
});
|
|
1311
|
+
hot.send('template:sync:action-response', {
|
|
1312
|
+
requestId: payload.requestId,
|
|
1313
|
+
approved,
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
hot.on('template:sync:response', (payload) => {
|
|
1317
|
+
deps.closeAllConfirmModals();
|
|
1318
|
+
if ('error' in payload && payload.config?.canSync === false) {
|
|
1319
|
+
available = false;
|
|
1320
|
+
}
|
|
1321
|
+
endSync();
|
|
1322
|
+
if ('cancelled' in payload && payload.cancelled) {
|
|
1323
|
+
deps.infoPopup({ title: 'Sync cancelled', content: payload.message, type: 'warning' });
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if ('error' in payload && typeof payload.error !== 'undefined') {
|
|
1327
|
+
if (payload.code === SYNC_IN_PROGRESS_CODE) {
|
|
1328
|
+
deps.infoPopup({ title: 'Sync already running', content: payload.error, type: 'warning' });
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
deps.infoPopup({ title: 'Sync error', content: payload.error, type: 'danger' });
|
|
1332
|
+
if (payload.refresh) {
|
|
1333
|
+
setTimeout(reload);
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if ('syncedAt' in payload && typeof payload.syncedAt !== 'undefined') {
|
|
1338
|
+
deps.infoPopup({
|
|
1339
|
+
title: 'Sync success',
|
|
1340
|
+
content: `Synced at ${new Date(payload.syncedAt).toLocaleString()}.\nBackup filename: ${payload.backupFilename}`,
|
|
1341
|
+
type: 'success',
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
// A response sent to a socket that has closed never arrives. Without this, the button would
|
|
1346
|
+
// spin, disabled, until the page is reloaded.
|
|
1347
|
+
hot.on('vite:ws:connect', () => {
|
|
1348
|
+
connected = true;
|
|
1349
|
+
});
|
|
1350
|
+
hot.on('vite:ws:disconnect', () => {
|
|
1351
|
+
connected = false;
|
|
1352
|
+
if (!syncing) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
deps.closeAllConfirmModals();
|
|
1356
|
+
endSync();
|
|
1357
|
+
deps.infoPopup({
|
|
1358
|
+
title: 'Sync result unknown',
|
|
1359
|
+
content: 'The connection to the dev server was lost during the sync. Check its terminal for the result before syncing again.',
|
|
1360
|
+
type: 'warning',
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
$button.addEventListener('click', (ev) => {
|
|
1364
|
+
ev.preventDefault();
|
|
1365
|
+
if (syncing || !available) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
// Sent now, the request would wait for a reconnect with the button spinning, and start a sync
|
|
1369
|
+
// whenever that happens. Say so instead, and let the click be repeated once connected.
|
|
1370
|
+
if (!isConnected()) {
|
|
1371
|
+
deps.infoPopup({
|
|
1372
|
+
title: 'No connection',
|
|
1373
|
+
content: 'The dev server is not connected. Check that it is running, then try again.',
|
|
1374
|
+
type: 'warning',
|
|
1375
|
+
});
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
syncing = true;
|
|
1379
|
+
applyState();
|
|
1380
|
+
hot.send('template:sync', {});
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1142
1383
|
|
|
1143
1384
|
/// <reference types="vite/client" />
|
|
1144
1385
|
// Applied unconditionally (even if the panel itself is hidden) — this is what makes the
|
|
@@ -1211,7 +1452,16 @@ function animatePopup($popup, type) {
|
|
|
1211
1452
|
function infoPopup(opts) {
|
|
1212
1453
|
const $popup = createPopupElement(opts);
|
|
1213
1454
|
const $closeButton = $popup.querySelector('.pp-dev-info__popup-title-close');
|
|
1455
|
+
let removed = false;
|
|
1456
|
+
let stopAutoDismiss;
|
|
1457
|
+
// Once only: closing it by hand used to leave its timer running, which removed it again and
|
|
1458
|
+
// counted it out of `activePopups` twice.
|
|
1214
1459
|
const removePopup = async () => {
|
|
1460
|
+
if (removed) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
removed = true;
|
|
1464
|
+
stopAutoDismiss?.();
|
|
1215
1465
|
await animatePopup($popup, 'exit');
|
|
1216
1466
|
$popup.remove();
|
|
1217
1467
|
updatePopupPositions();
|
|
@@ -1224,32 +1474,7 @@ function infoPopup(opts) {
|
|
|
1224
1474
|
animatePopup($popup, 'enter');
|
|
1225
1475
|
const duration = opts.duration ?? 10000;
|
|
1226
1476
|
if (duration > 0) {
|
|
1227
|
-
|
|
1228
|
-
let lastUpdate = Date.now();
|
|
1229
|
-
let isVisible = true;
|
|
1230
|
-
const scheduleDismiss = () => {
|
|
1231
|
-
if (!isVisible) {
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
const now = Date.now();
|
|
1235
|
-
const elapsed = now - lastUpdate;
|
|
1236
|
-
remainingTime -= elapsed;
|
|
1237
|
-
lastUpdate = now;
|
|
1238
|
-
if (remainingTime <= 0) {
|
|
1239
|
-
removePopup();
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1243
|
-
};
|
|
1244
|
-
// Handle visibility change
|
|
1245
|
-
document.addEventListener('visibilitychange', () => {
|
|
1246
|
-
isVisible = !document.hidden;
|
|
1247
|
-
if (isVisible) {
|
|
1248
|
-
lastUpdate = Date.now();
|
|
1249
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
requestAnimationFrame(scheduleDismiss);
|
|
1477
|
+
stopAutoDismiss = startAutoDismiss(duration, removePopup);
|
|
1253
1478
|
}
|
|
1254
1479
|
return { close: removePopup };
|
|
1255
1480
|
}
|
|
@@ -1372,82 +1597,9 @@ if (hot) {
|
|
|
1372
1597
|
hot.on('redirect', (data) => {
|
|
1373
1598
|
window.location.href = data.url;
|
|
1374
1599
|
});
|
|
1375
|
-
hot.on('client:config:update', (data) => {
|
|
1376
|
-
if (typeof data.config?.canSync === 'boolean') {
|
|
1377
|
-
if (data.config.canSync) {
|
|
1378
|
-
const $syncButton = document.getElementById('sync-template');
|
|
1379
|
-
if ($syncButton) {
|
|
1380
|
-
$syncButton.disabled = false;
|
|
1381
|
-
$syncButton.classList.remove('disabled');
|
|
1382
|
-
$syncButton.title = 'Sync template';
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
const $syncButton = document.getElementById('sync-template');
|
|
1387
|
-
if ($syncButton) {
|
|
1388
|
-
$syncButton.disabled = true;
|
|
1389
|
-
$syncButton.classList.add('disabled');
|
|
1390
|
-
$syncButton.title = 'Sync is unavailable on this instance';
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
1600
|
const $syncButton = document.getElementById('sync-template');
|
|
1396
1601
|
if ($syncButton) {
|
|
1397
|
-
hot
|
|
1398
|
-
// Keep the sync spinner running while a confirmation modal is shown — the sync
|
|
1399
|
-
// process is still in progress and only ends on `template:sync:response`.
|
|
1400
|
-
const approved = await confirmModal({
|
|
1401
|
-
title: payload.title,
|
|
1402
|
-
content: payload.content,
|
|
1403
|
-
confirmText: payload.confirmText,
|
|
1404
|
-
cancelText: payload.cancelText,
|
|
1405
|
-
});
|
|
1406
|
-
hot.send('template:sync:action-response', {
|
|
1407
|
-
requestId: payload.requestId,
|
|
1408
|
-
approved,
|
|
1409
|
-
});
|
|
1410
|
-
});
|
|
1411
|
-
hot.on('template:sync:response', (payload) => {
|
|
1412
|
-
closeAllConfirmModals();
|
|
1413
|
-
$syncButton.classList.remove('syncing');
|
|
1414
|
-
if ('cancelled' in payload && payload.cancelled) {
|
|
1415
|
-
infoPopup({
|
|
1416
|
-
title: 'Sync cancelled',
|
|
1417
|
-
content: payload.message,
|
|
1418
|
-
type: 'warning',
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
else if ('error' in payload && typeof payload.error !== 'undefined') {
|
|
1422
|
-
infoPopup({
|
|
1423
|
-
title: 'Sync error',
|
|
1424
|
-
content: payload.error,
|
|
1425
|
-
type: 'danger',
|
|
1426
|
-
});
|
|
1427
|
-
if (payload.refresh) {
|
|
1428
|
-
setTimeout(() => {
|
|
1429
|
-
window.location.reload();
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
else {
|
|
1433
|
-
$syncButton.disabled = true;
|
|
1434
|
-
$syncButton.classList.add('disabled');
|
|
1435
|
-
$syncButton.title = 'Sync is unavailable on this instance';
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
else if ('syncedAt' in payload && typeof payload.syncedAt !== 'undefined') {
|
|
1439
|
-
infoPopup({
|
|
1440
|
-
title: 'Sync success',
|
|
1441
|
-
content: `Synced at ${new Date(payload.syncedAt).toLocaleString()}.\nBackup filename: ${payload.backupFilename}`,
|
|
1442
|
-
type: 'success',
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
$syncButton.addEventListener('click', (ev) => {
|
|
1447
|
-
ev.preventDefault();
|
|
1448
|
-
$syncButton.classList.add('syncing');
|
|
1449
|
-
hot.send('template:sync', {});
|
|
1450
|
-
});
|
|
1602
|
+
initSyncButton(hot, $syncButton, { infoPopup, confirmModal, closeAllConfirmModals });
|
|
1451
1603
|
}
|
|
1452
1604
|
hot.on('page-variables:reload:response', (payload) => {
|
|
1453
1605
|
pendingReloadVariablesPopup?.close();
|