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.
@@ -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 { formatTimeJSX } from "../../misc/formatJSX";
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
- <div
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
- onClick={() => {
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 {formatTimeJSX(machineInfo.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
- </div>
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(-10_000_000);
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 { formatTimeJSX } from "../../misc/formatJSX";
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 {formatTimeJSX(config.info.lastUpdatedTime)}
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 { formatTimeJSX } from "../../misc/formatJSX";
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
- {formatTimeJSX(gitRefInfo.time)} AGO <span className={css.hsl(0, 0, 80).pad2(5, 2).italic}>{gitRefInfo.description}</span>
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(0).alignItems("center");
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
  }