querysub 0.260.0 → 0.261.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +2 -0
- package/package.json +1 -1
- package/src/-e-certs/EdgeCertController.ts +1 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
- package/src/3-path-functions/PathFunctionHelpers.ts +5 -6
- package/src/3-path-functions/PathFunctionRunner.ts +10 -3
- package/src/3-path-functions/PathFunctionRunnerMain.ts +2 -4
- package/src/4-deploy/deployFunctions.ts +82 -0
- package/src/4-deploy/deployGetFunctionsInner.ts +94 -0
- package/src/4-deploy/deployMain.ts +9 -109
- package/src/4-deploy/edgeBootstrap.ts +3 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -0
- package/src/4-deploy/edgeNodes.ts +7 -1
- package/src/4-querysub/Querysub.ts +45 -6
- package/src/deployManager/MachinesPage.tsx +3 -0
- package/src/deployManager/components/DeployPage.tsx +385 -0
- package/src/deployManager/components/DeployProgressView.tsx +135 -0
- package/src/deployManager/components/MachinesListPage.tsx +10 -9
- package/src/deployManager/components/ServiceDetailPage.tsx +1 -1
- package/src/deployManager/components/ServicesListPage.tsx +2 -2
- package/src/deployManager/components/deployButtons.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +1 -0
- package/src/deployManager/machineSchema.ts +77 -3
- package/src/deployManager/spec.txt +22 -19
- package/src/deployManager/urlParams.ts +1 -1
- package/src/diagnostics/NodeViewer.tsx +17 -2
- package/src/library-components/ATag.tsx +14 -9
- package/src/misc/formatJSX.tsx +1 -1
- package/src/misc.ts +8 -0
- package/src/server.ts +10 -10
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { deployGetFunctions } from "../../4-deploy/deployFunctions";
|
|
5
|
+
import { MachineServiceController, deployFunctionsWithProgress } from "../machineSchema";
|
|
6
|
+
import { functionSchema, FunctionSpec } from "../../3-path-functions/PathFunctionRunner";
|
|
7
|
+
import { getDomain } from "../../config";
|
|
8
|
+
import { RenderGitRefInfo, UpdateButtons, bigEmoji, buttonStyle } from "./deployButtons";
|
|
9
|
+
import { css } from "typesafecss";
|
|
10
|
+
import { timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
11
|
+
import { t } from "../../2-proxy/schema2";
|
|
12
|
+
import { mostCommon } from "../../misc";
|
|
13
|
+
import { Querysub } from "../../4-querysub/Querysub";
|
|
14
|
+
import { DeployProgressView, ProgressStage } from "./DeployProgressView";
|
|
15
|
+
import { URLParam } from "../../library-components/URLParam";
|
|
16
|
+
import { InputLabelURL } from "../../library-components/InputLabel";
|
|
17
|
+
|
|
18
|
+
module.hotreload = true;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const forceDeployNowURL = new URLParam("forceDeployNow", false);
|
|
22
|
+
const deployOnlyCodeURL = new URLParam("deployOnlyCode", false);
|
|
23
|
+
const deployOnlyUIURL = new URLParam("deployOnlyUI", false);
|
|
24
|
+
|
|
25
|
+
type FunctionKey = string;
|
|
26
|
+
|
|
27
|
+
function getFunctionKey(func: FunctionSpec): FunctionKey {
|
|
28
|
+
return `${func.DomainName}.${func.ModuleId}.${func.FunctionId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createFunctionMap(functions: FunctionSpec[]): Map<FunctionKey, FunctionSpec> {
|
|
32
|
+
const map = new Map<FunctionKey, FunctionSpec>();
|
|
33
|
+
for (const func of functions) {
|
|
34
|
+
map.set(getFunctionKey(func), func);
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type FunctionDiff = {
|
|
40
|
+
additions: FunctionSpec[];
|
|
41
|
+
removals: FunctionSpec[];
|
|
42
|
+
changes: { pending: FunctionSpec; live: FunctionSpec; }[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function calculateFunctionDiff(pendingFunctions: FunctionSpec[], liveFunctions: FunctionSpec[]): FunctionDiff {
|
|
46
|
+
const pendingMap = createFunctionMap(pendingFunctions);
|
|
47
|
+
const liveMap = createFunctionMap(liveFunctions);
|
|
48
|
+
|
|
49
|
+
const additions: FunctionSpec[] = [];
|
|
50
|
+
const removals: FunctionSpec[] = [];
|
|
51
|
+
const changes: { pending: FunctionSpec; live: FunctionSpec; }[] = [];
|
|
52
|
+
|
|
53
|
+
// Find additions and changes
|
|
54
|
+
for (const [key, pendingFunc] of pendingMap) {
|
|
55
|
+
const liveFunc = liveMap.get(key);
|
|
56
|
+
if (!liveFunc) {
|
|
57
|
+
additions.push(pendingFunc);
|
|
58
|
+
} else {
|
|
59
|
+
// If key matches, assume there are changes
|
|
60
|
+
changes.push({ pending: pendingFunc, live: liveFunc });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Find removals
|
|
65
|
+
for (const [key, liveFunc] of liveMap) {
|
|
66
|
+
if (!pendingMap.has(key)) {
|
|
67
|
+
removals.push(liveFunc);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { additions, removals, changes };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function RenderFunctionInfo({ func, label }: { func: FunctionSpec; label?: string }) {
|
|
75
|
+
return <div className={css.vbox(2).pad2(6).bord2(0, 0, 80).hsl(0, 0, 98)}>
|
|
76
|
+
{label && <div className={functionLabelStyle}>{label}</div>}
|
|
77
|
+
<div className={functionNameStyle}>
|
|
78
|
+
{func.FunctionId}
|
|
79
|
+
</div>
|
|
80
|
+
</div>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const diffViewList = css.vbox(40);
|
|
84
|
+
|
|
85
|
+
const moduleContainerStyle = css.vbox(3).pad2(8).hsla(0, 0, 0, 0.1);
|
|
86
|
+
const sectionTitleStyle = css.fontSize(16).boldStyle;
|
|
87
|
+
const moduleHeaderStyle = css.fontSize(13).boldStyle.colorhsl(0, 0, 30);
|
|
88
|
+
const functionNameStyle = css.boldStyle.colorhsl(0, 0, 10).fontSize(13);
|
|
89
|
+
const functionLabelStyle = css.fontSize(11).boldStyle.colorhsl(200, 70, 40);
|
|
90
|
+
|
|
91
|
+
const sectionContainerStyle = css.vbox(6);
|
|
92
|
+
const sectionListStyle = css.hbox(10).marginLeft(12).wrap;
|
|
93
|
+
const functionListStyle = css.hbox(6).wrap.marginLeft(8);
|
|
94
|
+
|
|
95
|
+
const filePathStyle = css.colorhsl(0, 0, 40);
|
|
96
|
+
const moduleTextStyle = css.colorhsl(0, 0, 30);
|
|
97
|
+
const oldFilePathStyle = css.colorhsl(0, 70, 40);
|
|
98
|
+
const newFilePathStyle = css.colorhsl(120, 70, 30);
|
|
99
|
+
const arrowStyle = css.colorhsl(0, 0, 40);
|
|
100
|
+
|
|
101
|
+
function countChanges(live: FunctionSpec, pending: FunctionSpec): number {
|
|
102
|
+
let changes = 0;
|
|
103
|
+
if (live.exportPathStr !== pending.exportPathStr) changes++;
|
|
104
|
+
if (live.FilePath !== pending.FilePath) changes++;
|
|
105
|
+
if (live.gitURL !== pending.gitURL) changes++;
|
|
106
|
+
if (live.gitRef !== pending.gitRef) changes++;
|
|
107
|
+
return changes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function GroupedFunctions({ functions, title }: { functions: FunctionSpec[]; title: string }) {
|
|
111
|
+
// Group functions by ModuleId
|
|
112
|
+
const moduleGroups = new Map<string, FunctionSpec[]>();
|
|
113
|
+
for (const func of functions) {
|
|
114
|
+
const moduleId = func.ModuleId;
|
|
115
|
+
let group = moduleGroups.get(moduleId);
|
|
116
|
+
if (!group) {
|
|
117
|
+
group = [];
|
|
118
|
+
moduleGroups.set(moduleId, group);
|
|
119
|
+
}
|
|
120
|
+
group.push(func);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sort module groups by count (largest first)
|
|
124
|
+
const sortedModuleEntries = Array.from(moduleGroups.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
125
|
+
|
|
126
|
+
const titleColor = title.includes("✅") && css.colorhsl(120, 70, 30) || css.colorhsl(0, 70, 40);
|
|
127
|
+
|
|
128
|
+
return <div className={sectionContainerStyle}>
|
|
129
|
+
<div className={sectionTitleStyle + titleColor}>{title}</div>
|
|
130
|
+
<div className={sectionListStyle}>
|
|
131
|
+
{sortedModuleEntries.map(([moduleId, funcs]) => (
|
|
132
|
+
<div key={moduleId} className={moduleContainerStyle}>
|
|
133
|
+
<div className={moduleHeaderStyle}>
|
|
134
|
+
<span className={filePathStyle}>{funcs[0].FilePath}</span>
|
|
135
|
+
<span className={moduleTextStyle}> - {moduleId}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div className={functionListStyle}>
|
|
138
|
+
{funcs.map((func, i) => (
|
|
139
|
+
RenderFunctionInfo({ func })
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</div>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function GroupedChanges({ changes, title }: { changes: { pending: FunctionSpec; live: FunctionSpec; }[]; title: string }) {
|
|
149
|
+
// Group changes by ModuleId
|
|
150
|
+
const moduleGroups = new Map<string, { pending: FunctionSpec; live: FunctionSpec; }[]>();
|
|
151
|
+
for (const change of changes) {
|
|
152
|
+
const moduleId = change.pending.ModuleId;
|
|
153
|
+
let group = moduleGroups.get(moduleId);
|
|
154
|
+
if (!group) {
|
|
155
|
+
group = [];
|
|
156
|
+
moduleGroups.set(moduleId, group);
|
|
157
|
+
}
|
|
158
|
+
group.push(change);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Sort changes within each group by number of changes (most changes first)
|
|
162
|
+
for (const [moduleId, moduleChanges] of moduleGroups.entries()) {
|
|
163
|
+
moduleChanges.sort((a, b) => countChanges(b.live, b.pending) - countChanges(a.live, a.pending));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort module groups by the most changed function in each group (highest individual change count first)
|
|
167
|
+
const sortedModuleEntries = Array.from(moduleGroups.entries()).sort((a, b) => {
|
|
168
|
+
const maxChangesA = Math.max(...a[1].map(change => countChanges(change.live, change.pending)));
|
|
169
|
+
const maxChangesB = Math.max(...b[1].map(change => countChanges(change.live, change.pending)));
|
|
170
|
+
return maxChangesB - maxChangesA;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return <div className={sectionContainerStyle}>
|
|
174
|
+
<div className={sectionTitleStyle.colorhsl(45, 70, 30)}>{title}</div>
|
|
175
|
+
<div className={sectionListStyle}>
|
|
176
|
+
{sortedModuleEntries.map(([moduleId, moduleChanges]) => {
|
|
177
|
+
// Check if file path changed (using first function as they're all the same)
|
|
178
|
+
const firstChange = moduleChanges[0];
|
|
179
|
+
const filePathChanged = firstChange.live.FilePath !== firstChange.pending.FilePath;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div key={moduleId} className={moduleContainerStyle}>
|
|
183
|
+
<div className={moduleHeaderStyle}>
|
|
184
|
+
{filePathChanged && (
|
|
185
|
+
<div>
|
|
186
|
+
<span className={oldFilePathStyle}>{firstChange.live.FilePath}</span>
|
|
187
|
+
<span className={arrowStyle}> → </span>
|
|
188
|
+
<span className={newFilePathStyle}>{firstChange.pending.FilePath}</span>
|
|
189
|
+
<span className={moduleTextStyle}> - {moduleId}</span>
|
|
190
|
+
</div>
|
|
191
|
+
) || (
|
|
192
|
+
<div>
|
|
193
|
+
<span className={filePathStyle}>{firstChange.pending.FilePath}</span>
|
|
194
|
+
<span className={moduleTextStyle}> - {moduleId}</span>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
<div className={functionListStyle}>
|
|
199
|
+
{moduleChanges.map((change, i) => (
|
|
200
|
+
<div key={i}>
|
|
201
|
+
{RenderFunctionInfo({ func: change.pending })}
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
</div>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function FunctionDiffView({ pendingFunctions, liveFunctions }: { pendingFunctions: FunctionSpec[]; liveFunctions: FunctionSpec[]; }) {
|
|
213
|
+
const diff = calculateFunctionDiff(pendingFunctions, liveFunctions);
|
|
214
|
+
|
|
215
|
+
return <div className={diffViewList}>
|
|
216
|
+
{/* Additions */}
|
|
217
|
+
{diff.additions.length > 0 &&
|
|
218
|
+
GroupedFunctions({
|
|
219
|
+
functions: diff.additions,
|
|
220
|
+
title: `✅ Added Functions (${diff.additions.length})`,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
{/* Removals */}
|
|
225
|
+
{diff.removals.length > 0 &&
|
|
226
|
+
GroupedFunctions({
|
|
227
|
+
functions: diff.removals,
|
|
228
|
+
title: `❌ Removed Functions (${diff.removals.length})`,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
{/* Changes */}
|
|
233
|
+
{diff.changes.length > 0 &&
|
|
234
|
+
GroupedChanges({
|
|
235
|
+
changes: diff.changes,
|
|
236
|
+
title: `🔄 Changed Functions (${diff.changes.length})`,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
{/* Show message when no changes */}
|
|
241
|
+
{diff.additions.length === 0 && diff.removals.length === 0 && diff.changes.length === 0 &&
|
|
242
|
+
<div className={filePathStyle.fontSize(14)}>
|
|
243
|
+
No changes detected between pending and live functions.
|
|
244
|
+
</div>
|
|
245
|
+
}
|
|
246
|
+
</div>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class DeployPage extends qreact.Component {
|
|
250
|
+
state = t.state({
|
|
251
|
+
isDeploying: t.boolean,
|
|
252
|
+
deployStartTime: t.atomic<number | undefined>(),
|
|
253
|
+
deployEndTime: t.atomic<number | undefined>(),
|
|
254
|
+
deploySuccess: t.boolean,
|
|
255
|
+
progressStages: t.atomic<ProgressStage[]>([]),
|
|
256
|
+
});
|
|
257
|
+
render() {
|
|
258
|
+
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
259
|
+
let gitInfo = controller.getGitInfo();
|
|
260
|
+
let pendingFunctions = controller.getPendingFunctions() || [];
|
|
261
|
+
let liveFunctions = controller.getLiveFunctions() || [];
|
|
262
|
+
|
|
263
|
+
let liveGitRef = mostCommon(liveFunctions.map(x => x.gitRef)) || "";
|
|
264
|
+
let pendingGitRef = gitInfo?.latestRef || "";
|
|
265
|
+
let anyUncommitted = !!gitInfo?.uncommitted.length;
|
|
266
|
+
|
|
267
|
+
return <div className={css.vbox(40).marginTop(20)}>
|
|
268
|
+
<div className={css.hbox(10)}>
|
|
269
|
+
<UpdateButtons services={[]} />
|
|
270
|
+
<button
|
|
271
|
+
className={buttonStyle.alignSelf("stretch").center.hsl(240, 70, 50).colorhsl(0, 0, 100)
|
|
272
|
+
+ (anyUncommitted && css.opacity(0.5))
|
|
273
|
+
+ (this.state.isDeploying && css.opacity(0.7))
|
|
274
|
+
}
|
|
275
|
+
title={anyUncommitted && `Must commit uncommitted changes before deploying` || this.state.isDeploying && "Deploying..." || ""}
|
|
276
|
+
onClick={async () => {
|
|
277
|
+
if (anyUncommitted || this.state.isDeploying) return;
|
|
278
|
+
|
|
279
|
+
this.state.isDeploying = true;
|
|
280
|
+
this.state.deployStartTime = Date.now();
|
|
281
|
+
this.state.deploySuccess = false;
|
|
282
|
+
this.state.progressStages = [];
|
|
283
|
+
this.state.deployEndTime = undefined;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await deployFunctionsWithProgress({
|
|
287
|
+
functionSpecs: pendingFunctions,
|
|
288
|
+
// NOTE: Old code should always be compatible with new code, so a long delay is fine. We don't want to have deal with bug reports today for bugs we fixed tomorrow, but we also don't want to force users to refresh, so this time is longer than the expected maximum session length.
|
|
289
|
+
// NOTE: Even if we want to deploy now, we can't deploy IMMEDIATELY. Give users at least 10 minutes to accept that that page will forcefully refresh.
|
|
290
|
+
notifyRefreshDelay: forceDeployNowURL.value ? timeInMinute * 10 : timeInHour * 12,
|
|
291
|
+
deployOnlyCode: deployOnlyCodeURL.value,
|
|
292
|
+
deployOnlyUI: deployOnlyUIURL.value,
|
|
293
|
+
onProgress: (config) => {
|
|
294
|
+
Querysub.commitLocal(() => {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
const newStage: ProgressStage = {
|
|
297
|
+
section: config.section,
|
|
298
|
+
progress: config.progress,
|
|
299
|
+
timestamp: now,
|
|
300
|
+
finished: config.progress >= 1,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Update or add stage
|
|
304
|
+
const currentStages = this.state.progressStages;
|
|
305
|
+
const stages = currentStages.slice();
|
|
306
|
+
const existingIndex = stages.findIndex((s: ProgressStage) => s.section === config.section);
|
|
307
|
+
if (existingIndex >= 0) {
|
|
308
|
+
stages[existingIndex] = newStage;
|
|
309
|
+
} else {
|
|
310
|
+
stages.push(newStage);
|
|
311
|
+
}
|
|
312
|
+
this.state.progressStages = stages;
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
Querysub.commitLocal(() => {
|
|
317
|
+
this.state.deploySuccess = true;
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
Querysub.commitLocal(() => {
|
|
321
|
+
this.state.deploySuccess = false;
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
Querysub.commitLocal(() => {
|
|
325
|
+
this.state.isDeploying = false;
|
|
326
|
+
this.state.deployEndTime = Date.now();
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
{this.state.isDeploying ? <>{bigEmoji("⚡")} Deploying...</> : <>{bigEmoji("🚀")} Deploy</>}
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div className={css.vbox(20)}>
|
|
336
|
+
<InputLabelURL
|
|
337
|
+
label="Force Deploy Now (very annoying for users, only to be used if the majority of the site broken, or data is being corrupted)"
|
|
338
|
+
flavor="large"
|
|
339
|
+
url={forceDeployNowURL}
|
|
340
|
+
checkbox
|
|
341
|
+
/>
|
|
342
|
+
<InputLabelURL
|
|
343
|
+
label="Deploy Code Only (no user refresh needed, just updates in the background)"
|
|
344
|
+
flavor="large"
|
|
345
|
+
url={deployOnlyCodeURL}
|
|
346
|
+
checkbox
|
|
347
|
+
/>
|
|
348
|
+
{/* We don't support UI only deploy, as I can't justify it. It... is just as slow, and still annoys users? So why would we do this? */}
|
|
349
|
+
{/* <InputLabelURL
|
|
350
|
+
label="Deploy Only UI (no code deployed. Annoying for users, but...?)"
|
|
351
|
+
flavor="large"
|
|
352
|
+
url={deployOnlyUIURL}
|
|
353
|
+
checkbox
|
|
354
|
+
/> */}
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div className={css.vbox(20).fontSize(20)}>
|
|
358
|
+
<div>
|
|
359
|
+
<b>New</b>
|
|
360
|
+
<RenderGitRefInfo gitRef={pendingGitRef} />
|
|
361
|
+
</div>
|
|
362
|
+
<div>
|
|
363
|
+
<b>Live</b>
|
|
364
|
+
<RenderGitRefInfo gitRef={liveGitRef} />
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
{DeployProgressView({
|
|
369
|
+
stages: this.state.progressStages,
|
|
370
|
+
isDeploying: this.state.isDeploying,
|
|
371
|
+
deploySuccess: this.state.deploySuccess,
|
|
372
|
+
deployStartTime: this.state.deployStartTime,
|
|
373
|
+
deployEndTime: this.state.deployEndTime,
|
|
374
|
+
})}
|
|
375
|
+
|
|
376
|
+
{liveGitRef === pendingGitRef && (
|
|
377
|
+
<div className={newFilePathStyle.fontSize(14).pad2(8).bord2(120, 50, 80).hsl(120, 30, 95)}>
|
|
378
|
+
✅ Everything is up to date - no changes to deploy
|
|
379
|
+
</div>
|
|
380
|
+
) || (
|
|
381
|
+
FunctionDiffView({ pendingFunctions, liveFunctions })
|
|
382
|
+
)}
|
|
383
|
+
</div>;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { css } from "typesafecss";
|
|
2
|
+
import { qreact } from "../../4-dom/qreact";
|
|
3
|
+
import { formatDateJSX } from "../../misc/formatJSX";
|
|
4
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
5
|
+
|
|
6
|
+
module.hotreload = true;
|
|
7
|
+
|
|
8
|
+
export type ProgressStage = {
|
|
9
|
+
section: string;
|
|
10
|
+
progress: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
finished: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const filePathStyle = css.colorhsl(0, 0, 40);
|
|
18
|
+
const newFilePathStyle = css.colorhsl(120, 70, 30);
|
|
19
|
+
const sectionHeaderStyle = css.fontSize(13).boldStyle;
|
|
20
|
+
const stageTextStyle = css.fontSize(12);
|
|
21
|
+
const stageContainerStyle = css.flexGrow(1);
|
|
22
|
+
const pendingStageBoxStyle = css.hbox(8).pad2(6).hsla(200, 30, 50, 0.1).bord2(200, 50, 80);
|
|
23
|
+
const finishedStageBoxStyle = css.hbox(8).pad2(6).hsla(120, 30, 50, 0.1).bord2(120, 50, 80);
|
|
24
|
+
const progressBarBackgroundStyle = css.fillWidth.height(4).hsl(0, 0, 90);
|
|
25
|
+
const progressBarFillStyle = css.height(4).hsl(200, 70, 50);
|
|
26
|
+
const sectionsStyle = css.vbox(6);
|
|
27
|
+
const sectionContainerStyle = css.vbox(4);
|
|
28
|
+
const stagesListStyle = css.hbox(2).wrap;
|
|
29
|
+
|
|
30
|
+
export function DeployProgressView({ stages, isDeploying, deploySuccess, deployStartTime, deployEndTime }: {
|
|
31
|
+
stages: ProgressStage[];
|
|
32
|
+
isDeploying: boolean;
|
|
33
|
+
deploySuccess: boolean;
|
|
34
|
+
deployStartTime: number | undefined;
|
|
35
|
+
deployEndTime: number | undefined;
|
|
36
|
+
}) {
|
|
37
|
+
if (!isDeploying && !deploySuccess) return null;
|
|
38
|
+
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const elapsed = (deployEndTime || now) - (deployStartTime || now);
|
|
41
|
+
|
|
42
|
+
// Separate pending and finished stages
|
|
43
|
+
const pendingStages = stages.filter(stage => !stage.finished);
|
|
44
|
+
const finishedStages = stages.filter(stage => stage.finished);
|
|
45
|
+
|
|
46
|
+
// Sort pending (oldest first), finished (newest first)
|
|
47
|
+
pendingStages.sort((a, b) => a.timestamp - b.timestamp);
|
|
48
|
+
finishedStages.sort((a, b) => b.timestamp - a.timestamp);
|
|
49
|
+
|
|
50
|
+
if (deploySuccess) {
|
|
51
|
+
return <div className={css.vbox(8).pad2(16).hsla(120, 40, 95, 0.9).bord2(120, 60, 50, 2).center}>
|
|
52
|
+
<style>{`
|
|
53
|
+
@keyframes celebrate {
|
|
54
|
+
0% { transform: scale(1) rotate(0deg); }
|
|
55
|
+
25% { transform: scale(1.1) rotate(-5deg); }
|
|
56
|
+
50% { transform: scale(1.2) rotate(5deg); }
|
|
57
|
+
75% { transform: scale(1.1) rotate(-2deg); }
|
|
58
|
+
100% { transform: scale(1) rotate(0deg); }
|
|
59
|
+
}
|
|
60
|
+
@keyframes sparkle {
|
|
61
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
62
|
+
50% { opacity: 0.7; transform: scale(1.2); }
|
|
63
|
+
}
|
|
64
|
+
.celebrate-animation {
|
|
65
|
+
animation: celebrate 2s ease-in-out infinite;
|
|
66
|
+
}
|
|
67
|
+
.sparkle-animation {
|
|
68
|
+
animation: sparkle 1.5s ease-in-out infinite;
|
|
69
|
+
}
|
|
70
|
+
`}</style>
|
|
71
|
+
<div className={css.hbox(8).alignItems("center")}>
|
|
72
|
+
<div className={css.fontSize(32) + " celebrate-animation"}>🎉</div>
|
|
73
|
+
<div className={newFilePathStyle.fontSize(18).boldStyle.colorhsl(120, 70, 30)}>
|
|
74
|
+
✅ Deploy Successful! 🚀
|
|
75
|
+
</div>
|
|
76
|
+
<div className={css.fontSize(28) + " sparkle-animation"}>✨</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div className={css.hbox(6).alignItems("center").justifyContent("center")}>
|
|
79
|
+
<div className={css.fontSize(16).colorhsl(120, 60, 40)}>
|
|
80
|
+
Completed in {formatTime(elapsed)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return <div className={css.vbox(6).pad2(12).hsla(200, 30, 50, 0.1).bord2(200, 50, 80)}>
|
|
87
|
+
<div className={filePathStyle.fontSize(14).boldStyle}>
|
|
88
|
+
🚀 Deploying... ({formatTime(elapsed)})
|
|
89
|
+
</div>
|
|
90
|
+
<div className={sectionsStyle}>
|
|
91
|
+
{/* Pending stages */}
|
|
92
|
+
{pendingStages.length > 0 && (
|
|
93
|
+
<div className={sectionContainerStyle}>
|
|
94
|
+
<div className={sectionHeaderStyle.colorhsl(200, 70, 30)}>In Progress</div>
|
|
95
|
+
<div className={stagesListStyle}>
|
|
96
|
+
{pendingStages.map((stage, i) => {
|
|
97
|
+
const progressPercent = Math.round(stage.progress * 100);
|
|
98
|
+
return <div key={i} className={pendingStageBoxStyle}>
|
|
99
|
+
<div className={stageContainerStyle}>
|
|
100
|
+
<div className={stageTextStyle}>
|
|
101
|
+
⏳ {stage.section} ({progressPercent}%)
|
|
102
|
+
</div>
|
|
103
|
+
<div className={progressBarBackgroundStyle}>
|
|
104
|
+
<div
|
|
105
|
+
className={progressBarFillStyle}
|
|
106
|
+
style={{ width: `${progressPercent}%` }}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>;
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Finished stages */}
|
|
117
|
+
{finishedStages.length > 0 && (
|
|
118
|
+
<div className={sectionContainerStyle}>
|
|
119
|
+
<div className={sectionHeaderStyle.colorhsl(120, 70, 30)}>Completed</div>
|
|
120
|
+
<div className={stagesListStyle}>
|
|
121
|
+
{finishedStages.map((stage, i) => (
|
|
122
|
+
<div key={i} className={finishedStageBoxStyle}>
|
|
123
|
+
<div className={stageContainerStyle}>
|
|
124
|
+
<div className={stageTextStyle}>
|
|
125
|
+
✓ {stage.section}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>;
|
|
135
|
+
}
|
|
@@ -7,9 +7,10 @@ import { Querysub } from "../../4-querysub/QuerysubController";
|
|
|
7
7
|
import { currentViewParam, selectedMachineIdParam } from "../urlParams";
|
|
8
8
|
import { formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
9
|
import { sort } from "socket-function/src/misc";
|
|
10
|
-
import {
|
|
10
|
+
import { formatDateJSX } from "../../misc/formatJSX";
|
|
11
11
|
import { DeployMachineButtons, RenderGitRefInfo } from "./deployButtons";
|
|
12
12
|
import { isDefined } from "../../misc";
|
|
13
|
+
import { Anchor, createLink } from "../../library-components/ATag";
|
|
13
14
|
|
|
14
15
|
export class MachinesListPage extends qreact.Component {
|
|
15
16
|
state = t.state({
|
|
@@ -130,7 +131,8 @@ export class MachinesListPage extends qreact.Component {
|
|
|
130
131
|
}, 0);
|
|
131
132
|
|
|
132
133
|
return <div className={css.hbox(10)}>
|
|
133
|
-
<
|
|
134
|
+
<Anchor
|
|
135
|
+
noStyles
|
|
134
136
|
className={
|
|
135
137
|
css.pad2(12).bord2(0, 0, 20).button
|
|
136
138
|
+ (
|
|
@@ -140,18 +142,17 @@ export class MachinesListPage extends qreact.Component {
|
|
|
140
142
|
)
|
|
141
143
|
+ (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
|
|
142
144
|
}
|
|
143
|
-
|
|
145
|
+
values={this.state.isDeleteMode ? [] : [currentViewParam.getOverride("machine-detail"), selectedMachineIdParam.getOverride(machineId)]}
|
|
146
|
+
onMouseDown={(e) => {
|
|
144
147
|
if (this.state.isDeleteMode) {
|
|
145
148
|
if (this.state.selectedForDeletion[machineId]) {
|
|
146
149
|
delete this.state.selectedForDeletion[machineId];
|
|
147
150
|
} else {
|
|
148
151
|
this.state.selectedForDeletion[machineId] = true;
|
|
149
152
|
}
|
|
150
|
-
} else {
|
|
151
|
-
currentViewParam.value = "machine-detail";
|
|
152
|
-
selectedMachineIdParam.value = machineId;
|
|
153
153
|
}
|
|
154
|
-
}}
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
155
156
|
<div className={css.hbox(12)}>
|
|
156
157
|
{this.state.isDeleteMode && (
|
|
157
158
|
<div className={css.flexShrink0}>
|
|
@@ -169,7 +170,7 @@ export class MachinesListPage extends qreact.Component {
|
|
|
169
170
|
</div>}
|
|
170
171
|
<div className={css.vbox(4)}>
|
|
171
172
|
<div>
|
|
172
|
-
Last heartbeat {
|
|
173
|
+
Last heartbeat {formatDateJSX(machineInfo.heartbeat)}
|
|
173
174
|
</div>
|
|
174
175
|
<RenderGitRefInfo gitRef={machineInfo.gitRef} />
|
|
175
176
|
</div>
|
|
@@ -185,7 +186,7 @@ export class MachinesListPage extends qreact.Component {
|
|
|
185
186
|
</div>
|
|
186
187
|
</div>
|
|
187
188
|
</div>
|
|
188
|
-
</
|
|
189
|
+
</Anchor>
|
|
189
190
|
<DeployMachineButtons machines={[machineInfo]} />
|
|
190
191
|
</div>;
|
|
191
192
|
})}
|
|
@@ -133,7 +133,7 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
133
133
|
onData: async (data: string) => {
|
|
134
134
|
Querysub.localCommit(() => {
|
|
135
135
|
let fullData = this.state.watchingOutputs[outputKey].data + data;
|
|
136
|
-
fullData = fullData.slice(-
|
|
136
|
+
fullData = fullData.slice(-100_000);
|
|
137
137
|
this.state.watchingOutputs[outputKey].data = fullData;
|
|
138
138
|
});
|
|
139
139
|
}
|
|
@@ -12,7 +12,7 @@ import { Anchor } from "../../library-components/ATag";
|
|
|
12
12
|
import { isPublic } from "../../config";
|
|
13
13
|
import { UpdateButtons, UpdateServiceButtons } from "./deployButtons";
|
|
14
14
|
import { isDefined } from "../../misc";
|
|
15
|
-
import {
|
|
15
|
+
import { formatDateJSX } from "../../misc/formatJSX";
|
|
16
16
|
|
|
17
17
|
module.hotreload = true;
|
|
18
18
|
|
|
@@ -108,7 +108,7 @@ export class ServicesListPage extends qreact.Component {
|
|
|
108
108
|
</div>
|
|
109
109
|
</div>
|
|
110
110
|
<div>
|
|
111
|
-
Updated {
|
|
111
|
+
Updated {formatDateJSX(config.info.lastUpdatedTime)}
|
|
112
112
|
</div>
|
|
113
113
|
</div>
|
|
114
114
|
</Anchor>
|
|
@@ -5,7 +5,7 @@ import { showFullscreenModal } from "../../5-diagnostics/FullscreenModal";
|
|
|
5
5
|
import { InputLabel } from "../../library-components/InputLabel";
|
|
6
6
|
import { css } from "../../4-dom/css";
|
|
7
7
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
8
|
-
import {
|
|
8
|
+
import { formatDateJSX } from "../../misc/formatJSX";
|
|
9
9
|
import { MachineController } from "../machineController";
|
|
10
10
|
import { closeAllModals } from "../../5-diagnostics/Modal";
|
|
11
11
|
import { unique } from "../../misc";
|
|
@@ -28,12 +28,12 @@ export class RenderGitRefInfo extends qreact.Component<{
|
|
|
28
28
|
});
|
|
29
29
|
if (!gitRefInfo) return undefined;
|
|
30
30
|
return <div className={css.fontWeight("normal")}>
|
|
31
|
-
{
|
|
31
|
+
{formatDateJSX(gitRefInfo.time)} AGO <span className={css.hsl(0, 0, 80).pad2(5, 2).italic}>{gitRefInfo.description}</span>
|
|
32
32
|
</div>;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export const buttonStyle = css.pad2(12, 8).button.bord2(0, 0, 20).fontWeight("bold").vbox(
|
|
36
|
+
export const buttonStyle = css.pad2(12, 8).button.bord2(0, 0, 20).fontWeight("bold").vbox(2).alignItems("center");
|
|
37
37
|
|
|
38
38
|
export class UpdateButtons extends qreact.Component<{
|
|
39
39
|
services: ServiceConfig[];
|
|
@@ -309,6 +309,7 @@ async function removeOldNodeId(screenName: string) {
|
|
|
309
309
|
let screenNameFile = os.homedir() + "/" + SERVICE_FOLDER + screenName + "/" + SERVICE_NODE_FILE_NAME;
|
|
310
310
|
if (fs.existsSync(screenNameFile)) {
|
|
311
311
|
let nodeId = await fs.promises.readFile(screenNameFile, "utf8");
|
|
312
|
+
console.log(green(`Removing node if for dead service on ${screenNameFile}, node id ${nodeId}`));
|
|
312
313
|
await fs.promises.unlink(screenNameFile);
|
|
313
314
|
await forceRemoveNode(nodeId);
|
|
314
315
|
}
|