querysub 0.252.0 → 0.253.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.252.0",
3
+ "version": "0.253.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -22,7 +22,7 @@
22
22
  "js-sha512": "^0.9.0",
23
23
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
24
24
  "pako": "^2.1.0",
25
- "socket-function": "^0.133.0",
25
+ "socket-function": "^0.134.0",
26
26
  "terser": "^5.31.0",
27
27
  "typesafecss": "^0.22.0",
28
28
  "yaml": "^2.5.0",
@@ -354,6 +354,7 @@ export class PathFunctionRunner {
354
354
  let runCallPromise = self.runCall(call, functionSpec);
355
355
  void runCallPromise.finally(() => {
356
356
  outstandingCalls--;
357
+ // IMPORTANT! We remove the call, NOT for GC, but! Because if the call writes are rejected (due to contention), then the Result will also be rejected, changing it back to undefined, so (as long as little enough time has passed), we will run the call again! Or rather, we will try to, as long as runningCalls doesn't have the value!
357
358
  runningCalls.delete(call.CallId);
358
359
  });
359
360
  logErrors(runCallPromise);
@@ -11,6 +11,40 @@ export async function getGitRefLive(gitDir = ".") {
11
11
  return (await runPromise(`git rev-parse HEAD`, { cwd: gitDir })).trim();
12
12
  }
13
13
 
14
+ export async function getGitUncommitted(gitDir = "."): Promise<string[]> {
15
+ return (await runPromise(`git status --porcelain`, { cwd: gitDir })).split("\n").map(x => x.trim()).filter(x => x);
16
+ }
17
+
18
+ export async function getLatestRefOnUpstreamBranch(gitDir = ".") {
19
+ await runPromise(`git fetch`, { cwd: gitDir });
20
+ return (await runPromise(`git rev-parse @{upstream}`, { cwd: gitDir })).trim();
21
+ }
22
+
23
+ export async function commitAndPush(config: {
24
+ gitDir: string;
25
+ message: string;
26
+ }) {
27
+ await runPromise(`git add --all`, { cwd: config.gitDir });
28
+ await runPromise(`git commit -m "${config.message}"`, { cwd: config.gitDir });
29
+ await runPromise(`git push`, { cwd: config.gitDir });
30
+ }
31
+
32
+ export async function getGitRefInfo(config: {
33
+ gitDir: string;
34
+ ref: string;
35
+ }): Promise<{
36
+ time: number;
37
+ description: string;
38
+ }> {
39
+ const timeOutput = await runPromise(`git show --format="%ct" -s ${config.ref}`, { cwd: config.gitDir });
40
+ const descriptionOutput = await runPromise(`git show --format="%s" -s ${config.ref}`, { cwd: config.gitDir });
41
+
42
+ return {
43
+ time: parseInt(timeOutput.trim(), 10) * 1000,
44
+ description: descriptionOutput.trim()
45
+ };
46
+ }
47
+
14
48
  export const getGitURLSync = cache(function getGitURLSync(gitDir?: string) {
15
49
  return child_process.execSync(`git remote get-url origin`, { cwd: gitDir || "." }).toString().trim();
16
50
  });
@@ -20,7 +54,6 @@ export const getGitRefSync = cache(function getGitRefSync(gitDir?: string) {
20
54
 
21
55
  export const setGitRef = measureWrap(async function setGitRef(config: {
22
56
  gitFolder: string;
23
- repoUrl: string;
24
57
  gitRef: string;
25
58
  }) {
26
59
  await fs.promises.mkdir(config.gitFolder, { recursive: true });
@@ -1,12 +1,13 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../4-dom/qreact";
3
- import { DeployController } from "./machineSchema";
3
+ import { MachineServiceController } from "./machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { currentViewParam, selectedServiceIdParam, selectedMachineIdParam } from "./urlParams";
6
6
  import { ServicesListPage } from "./components/ServicesListPage";
7
7
  import { MachinesListPage } from "./components/MachinesListPage";
8
8
  import { ServiceDetailPage } from "./components/ServiceDetailPage";
9
9
  import { MachineDetailPage } from "./components/MachineDetailPage";
10
+ import { Anchor } from "../library-components/ATag";
10
11
 
11
12
  export class MachinesPage extends qreact.Component {
12
13
  private renderTabs() {
@@ -19,24 +20,20 @@ export class MachinesPage extends qreact.Component {
19
20
  { key: "services", label: "Services", otherKeys: ["service-detail"] },
20
21
  ].map(tab => {
21
22
  let isActive = currentViewParam.value === tab.key || tab.otherKeys.includes(currentViewParam.value);
22
- return <div key={tab.key}
23
+ return <Anchor noStyles key={tab.key}
24
+ values={[currentViewParam.getOverride(tab.key as any), selectedServiceIdParam.getOverride(""), selectedMachineIdParam.getOverride("")]}
23
25
  className={css.pad2(12, 8).button
24
26
  + (isActive && css.hsl(0, 0, 100).bord2(210, 50, 50))
25
27
  + (!isActive && css.hsl(0, 0, 70).colorhsl(0, 0, 50).bord2(0, 0, 50))
26
- }
27
- onClick={() => {
28
- currentViewParam.value = tab.key as any;
29
- selectedServiceIdParam.value = "";
30
- selectedMachineIdParam.value = "";
31
- }}>
28
+ }>
32
29
  {tab.label}
33
- </div>;
30
+ </Anchor>;
34
31
  })}
35
32
  </div>;
36
33
  }
37
34
 
38
35
  render() {
39
- let controller = DeployController(SocketFunction.browserNodeId());
36
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
40
37
  let serviceConfigType = controller.getServiceConfigType();
41
38
  if (!serviceConfigType) return <div>Loading...</div>;
42
39
 
@@ -1,17 +1,18 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, DeployController, ServiceConfig } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { currentViewParam, selectedMachineIdParam, selectedServiceIdParam } from "../urlParams";
6
6
  import { formatVeryNiceDateTime } from "socket-function/src/formatting/format";
7
7
  import { sort } from "socket-function/src/misc";
8
+ import { Anchor } from "../../library-components/ATag";
8
9
 
9
10
  export class MachineDetailPage extends qreact.Component {
10
11
  render() {
11
12
  const selectedMachineId = selectedMachineIdParam.value;
12
13
  if (!selectedMachineId) return <div>No machine selected</div>;
13
14
 
14
- let controller = DeployController(SocketFunction.browserNodeId());
15
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
15
16
  let machineInfo = controller.getMachineInfo(selectedMachineId);
16
17
  let serviceList = controller.getServiceList();
17
18
 
@@ -77,14 +78,10 @@ export class MachineDetailPage extends qreact.Component {
77
78
  <h3>⚠️ Configured Services Not Running ({configuredButNotRunning.length})</h3>
78
79
  <div className={css.vbox(8)}>
79
80
  {configuredButNotRunning.map(([serviceId, serviceConfig]) => (
80
- <div key={serviceId}
81
- className={
82
- css.pad2(12).button.bord2(0, 0, 20).hsl(0, 70, 90)
83
- }
84
- onClick={() => {
85
- currentViewParam.value = "service-detail";
86
- selectedServiceIdParam.value = serviceId;
87
- }}>
81
+ <Anchor noStyles
82
+ key={serviceId}
83
+ values={[currentViewParam.getOverride("service-detail"), selectedServiceIdParam.getOverride(serviceId)]}
84
+ className={css.pad2(12).button.bord2(0, 0, 20).hsl(0, 70, 90)}>
88
85
  <div className={css.hbox(12)}>
89
86
  <div className={css.flexGrow(1)}>
90
87
  {serviceConfig.info.title} ({serviceConfig.parameters.key} / {serviceId})
@@ -96,7 +93,7 @@ export class MachineDetailPage extends qreact.Component {
96
93
  Deploy: {serviceConfig.parameters.deploy ? "Yes" : "No"}
97
94
  </div>
98
95
  </div>
99
- </div>
96
+ </Anchor>
100
97
  ))}
101
98
  </div>
102
99
  </div>
@@ -118,15 +115,10 @@ export class MachineDetailPage extends qreact.Component {
118
115
  }
119
116
 
120
117
  return (
121
- <div key={serviceId}
122
- className={
123
- css.pad2(12).button.bord2(0, 0, 20)
124
- + backgroundColor
125
- }
126
- onClick={() => {
127
- currentViewParam.value = "service-detail";
128
- selectedServiceIdParam.value = serviceId;
129
- }}>
118
+ <Anchor noStyles
119
+ key={serviceId}
120
+ values={[currentViewParam.getOverride("service-detail"), selectedServiceIdParam.getOverride(serviceId)]}
121
+ className={css.pad2(12).button.bord2(0, 0, 20) + backgroundColor}>
130
122
  <div className={css.hbox(12)}>
131
123
  <div className={css.flexGrow(1)}>
132
124
  {serviceConfig && <>{serviceConfig.info.title} ({serviceConfig.parameters.key})</> || <>serviceId = {serviceId}</>}
@@ -148,7 +140,7 @@ export class MachineDetailPage extends qreact.Component {
148
140
  </div>
149
141
  )}
150
142
  </div>
151
- </div>
143
+ </Anchor>
152
144
  );
153
145
  })}
154
146
  </div>
@@ -1,12 +1,15 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, DeployController } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  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";
11
+ import { DeployMachineButtons, RenderGitRefInfo } from "./deployButtons";
12
+ import { isDefined } from "../../misc";
10
13
 
11
14
  export class MachinesListPage extends qreact.Component {
12
15
  state = t.state({
@@ -16,7 +19,7 @@ export class MachinesListPage extends qreact.Component {
16
19
  });
17
20
 
18
21
  render() {
19
- let controller = DeployController(SocketFunction.browserNodeId());
22
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
20
23
  let machineList = controller.getMachineList();
21
24
 
22
25
  if (!machineList) return <div>Loading machines...</div>;
@@ -52,6 +55,8 @@ export class MachinesListPage extends qreact.Component {
52
55
  </button>
53
56
  )}
54
57
 
58
+ <DeployMachineButtons machines={machines.map(x => x[1]).filter(isDefined)} />
59
+
55
60
  {this.state.isDeleteMode && (
56
61
  <div className={css.hbox(8)}>
57
62
  <div className={css.pad2(8, 4)}>
@@ -116,58 +121,64 @@ export class MachinesListPage extends qreact.Component {
116
121
  return acc + (service.totalTimesLaunched || 0);
117
122
  }, 0);
118
123
 
119
- return <div key={machineId}
120
- className={
121
- css.pad2(12).bord2(0, 0, 20).button
122
- + (
123
- failingServices.length > 0 && css.hsl(0, 50, 60)
124
- || isMachineDead && css.hsl(0, 0, 50)
125
- || css.hsl(0, 0, 100)
126
- )
127
- + (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
128
- }
129
- onClick={() => {
130
- if (this.state.isDeleteMode) {
131
- if (this.state.selectedForDeletion[machineId]) {
132
- delete this.state.selectedForDeletion[machineId];
124
+ return <div className={css.hbox(10)}>
125
+ <div
126
+ className={
127
+ css.pad2(12).bord2(0, 0, 20).button
128
+ + (
129
+ failingServices.length > 0 && css.hsl(0, 50, 60)
130
+ || isMachineDead && css.hsl(0, 0, 50)
131
+ || css.hsl(0, 0, 100)
132
+ )
133
+ + (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
134
+ }
135
+ onClick={() => {
136
+ if (this.state.isDeleteMode) {
137
+ if (this.state.selectedForDeletion[machineId]) {
138
+ delete this.state.selectedForDeletion[machineId];
139
+ } else {
140
+ this.state.selectedForDeletion[machineId] = true;
141
+ }
133
142
  } else {
134
- this.state.selectedForDeletion[machineId] = true;
143
+ currentViewParam.value = "machine-detail";
144
+ selectedMachineIdParam.value = machineId;
135
145
  }
136
- } else {
137
- currentViewParam.value = "machine-detail";
138
- selectedMachineIdParam.value = machineId;
139
- }
140
- }}>
141
- <div className={css.hbox(12)}>
142
- {this.state.isDeleteMode && (
143
- <div className={css.flexShrink0}>
144
- <input
145
- type="checkbox"
146
- checked={isSelected}
147
- onChange={() => { }} // Controlled by onClick above
148
- onClick={(e) => e.stopPropagation()} // Prevent double-toggle
149
- className={css.size(16, 16)}
150
- />
151
- </div>
152
- )}
153
- {isMachineDead && <div className={css.colorhsl(0, 80, 50)}>
154
- ⚠️ Machine is likely dead
155
- </div>}
156
- <div>
157
- Last heartbeat {formatVeryNiceDateTime(machineInfo.heartbeat)}
158
- </div>
159
- <div className={css.vbox(4).flexGrow(1)}>
160
- <div>
161
- {machineId}
162
- </div>
163
- <div>
164
- {machineInfo.info["getExternalIP"]}
146
+ }}>
147
+ <div className={css.hbox(12)}>
148
+ {this.state.isDeleteMode && (
149
+ <div className={css.flexShrink0}>
150
+ <input
151
+ type="checkbox"
152
+ checked={isSelected}
153
+ onChange={() => { }} // Controlled by onClick above
154
+ onClick={(e) => e.stopPropagation()} // Prevent double-toggle
155
+ className={css.size(16, 16)}
156
+ />
157
+ </div>
158
+ )}
159
+ {isMachineDead && <div className={css.colorhsl(0, 80, 50)}>
160
+ ⚠️ Machine is likely dead
161
+ </div>}
162
+ <div className={css.vbox(4)}>
163
+ <div>
164
+ Last heartbeat {formatTimeJSX(machineInfo.heartbeat)}
165
+ </div>
166
+ <RenderGitRefInfo gitRef={machineInfo.gitRef} />
165
167
  </div>
166
- <div>
167
- {serviceCount} services {failingServices.length > 0 ? `(${failingServices.length} failing)` : ""} • {totalLaunches} launches
168
+ <div className={css.vbox(4).flexGrow(1)}>
169
+ <div>
170
+ {machineId}
171
+ </div>
172
+ <div>
173
+ {machineInfo.info["getExternalIP"]}
174
+ </div>
175
+ <div>
176
+ {serviceCount} services {failingServices.length > 0 ? `(${failingServices.length} failing)` : ""} • {totalLaunches} launches
177
+ </div>
168
178
  </div>
169
179
  </div>
170
180
  </div>
181
+ <DeployMachineButtons machines={[machineInfo]} />
171
182
  </div>;
172
183
  })}
173
184
  </div>
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, DeployController, ServiceConfig } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/QuerysubController";
@@ -13,6 +13,7 @@ import { Button } from "../../library-components/Button";
13
13
  import { isDefined } from "../../misc";
14
14
  import { watchScreenOutput, stopWatchingScreenOutput } from "../machineController";
15
15
  import { getPathStr2 } from "../../path";
16
+ import { Anchor } from "../../library-components/ATag";
16
17
  import { ScrollOnMount } from "../../library-components/ScrollOnMount";
17
18
  import { StickyBottomScroll } from "../../library-components/StickyBottomScroll";
18
19
  import { PrimitiveDisplay } from "../../diagnostics/logs/ObjectDisplay";
@@ -179,7 +180,7 @@ type ServiceConfig = ${serviceConfigType}
179
180
  model.setValue(`const config: ServiceConfig = ${newConfigStr}`);
180
181
  }
181
182
  }
182
- await DeployController(SocketFunction.browserNodeId()).setServiceConfig.promise(selectedServiceId, updatedConfig);
183
+ await MachineServiceController(SocketFunction.browserNodeId()).setServiceConfigs.promise([updatedConfig]);
183
184
  } catch (error) {
184
185
  Querysub.localCommit(() => {
185
186
  this.state.saveError = error instanceof Error ? error.message : String(error);
@@ -198,7 +199,7 @@ type ServiceConfig = ${serviceConfigType}
198
199
  const selectedServiceId = selectedServiceIdParam.value;
199
200
  if (!selectedServiceId) return <div>No service selected</div>;
200
201
 
201
- let controller = DeployController(SocketFunction.browserNodeId());
202
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
202
203
  let originalConfig = controller.getServiceConfig(selectedServiceId);
203
204
  let serviceConfigType = controller.getServiceConfigType();
204
205
 
@@ -371,15 +372,12 @@ type ServiceConfig = ${serviceConfigType}
371
372
  </div>
372
373
  )}
373
374
 
374
- <div
375
+ <Anchor noStyles
376
+ values={[currentViewParam.getOverride("machine-detail"), selectedMachineIdParam.getOverride(machineId)]}
375
377
  className={css.button.pad2(16, 8).bord2(0, 0, 10) + css.hsl(200, 70, 90)}
376
- onClick={() => {
377
- currentViewParam.value = "machine-detail";
378
- selectedMachineIdParam.value = machineId;
379
- }}
380
378
  >
381
379
  View Machine
382
- </div>
380
+ </Anchor>
383
381
 
384
382
 
385
383
  <div
@@ -1,18 +1,25 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { DeployController, ServiceConfig } from "../machineSchema";
3
+ import { MachineServiceController, ServiceConfig } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/QuerysubController";
7
7
  import { currentViewParam, selectedServiceIdParam } from "../urlParams";
8
8
  import { formatNiceDateTime, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
9
- import { sort } from "socket-function/src/misc";
9
+ import { sort, timeInMinute } from "socket-function/src/misc";
10
10
  import { cache } from "socket-function/src/caching";
11
+ import { Anchor } from "../../library-components/ATag";
12
+ import { isPublic } from "../../config";
13
+ import { UpdateButtons, UpdateServiceButtons } from "./deployButtons";
14
+ import { isDefined } from "../../misc";
15
+ import { formatTimeJSX } from "../../misc/formatJSX";
16
+
17
+ module.hotreload = true;
11
18
 
12
19
  export class ServicesListPage extends qreact.Component {
13
20
 
14
21
  render() {
15
- let controller = DeployController(SocketFunction.browserNodeId());
22
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
16
23
  let serviceList = controller.getServiceList();
17
24
 
18
25
  if (!serviceList) return <div>Loading services...</div>;
@@ -23,7 +30,7 @@ export class ServicesListPage extends qreact.Component {
23
30
  let getMachineInfo = cache((machineId: string) => controller.getMachineInfo(machineId));
24
31
 
25
32
  return <div className={css.vbox(16)}>
26
- <div className={css.hbox(12)}>
33
+ <div className={css.hbox(12).wrap}>
27
34
  <h2 className={css.flexGrow(1)}>Services</h2>
28
35
  <button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 100)}
29
36
  onClick={() => {
@@ -36,14 +43,14 @@ export class ServicesListPage extends qreact.Component {
36
43
  parameters: {
37
44
  key: newServiceId,
38
45
  repoUrl: gitInfo.repoUrl,
39
- gitRef: gitInfo.gitRef,
46
+ gitRef: gitInfo.latestRef,
40
47
  command: "ping 1.1.1.1",
41
48
  deploy: false
42
49
  },
43
50
  info: {
44
51
  title: `New Service created at ${formatNiceDateTime(Date.now())}`,
45
52
  notes: "Notes",
46
- lastUpdatedTime: Date.now()
53
+ lastUpdatedTime: Date.now(),
47
54
  }
48
55
  };
49
56
 
@@ -56,10 +63,11 @@ export class ServicesListPage extends qreact.Component {
56
63
  }}>
57
64
  + New Service
58
65
  </button>
66
+ <UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
59
67
  </div>
60
68
  <div className={css.vbox(8)}>
61
69
  {services.map(([serviceId, config]) => {
62
- if (!config) return <div key={serviceId}>Config is broken?</div>;
70
+ if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;
63
71
  let failingMachines = config.machineIds.filter(machineId => {
64
72
  let machineInfo = getMachineInfo(machineId);
65
73
  return machineInfo?.services[serviceId]?.errorFromLastRun;
@@ -78,33 +86,35 @@ export class ServicesListPage extends qreact.Component {
78
86
  return acc + (machineInfo?.services[serviceId]?.totalTimesLaunched || 0);
79
87
  }, 0);
80
88
  let unknown = config.machineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
81
- return <div key={serviceId}
82
- className={
83
- css.pad2(12).button.bord2(0, 0, 20)
84
- + (
85
- failingMachines.length > 0 && css.hsl(0, 50, 60)
86
- || config.parameters.deploy && css.hsl(0, 0, 100)
87
- || !config.parameters.deploy && css.hsl(0, 0, 70)
88
- )
89
- }
90
- onClick={() => {
91
- currentViewParam.value = "service-detail";
92
- selectedServiceIdParam.value = serviceId;
93
- }}>
94
- <div className={css.hbox(12)}>
95
- <div className={css.vbox(4).flexGrow(1)}>
96
- <div className={css.fontSize(14).boldStyle}>{config.info.title}</div>
97
- <div>{config.parameters.key}</div>
89
+ return <div className={css.hbox(10)}>
90
+ <Anchor noStyles key={serviceId}
91
+ values={[currentViewParam.getOverride("service-detail"), selectedServiceIdParam.getOverride(serviceId)]}
92
+ className={
93
+ css.pad2(12).button.bord2(0, 0, 20)
94
+ + (
95
+ failingMachines.length > 0 && css.hsl(0, 50, 60)
96
+ || config.parameters.deploy && css.hsl(0, 0, 100)
97
+ || !config.parameters.deploy && css.hsl(0, 0, 70)
98
+ )
99
+ }
100
+ >
101
+ <div className={css.hbox(12)}>
102
+ <div className={css.vbox(4).flexGrow(1)}>
103
+ <div className={css.fontSize(14).boldStyle}>{config.info.title}</div>
104
+ <div>{config.parameters.key}</div>
105
+ <div>
106
+ {config.machineIds.length} configured {failingMachines.length > 0 && `(${failingMachines.length} failing)`} {missingMachines.length > 0 && `(${missingMachines.length} machine hasn't run service yet)`} {unknown > 0 && `(${unknown} unknown)`} • {totalLaunches} launches •
107
+ Deploy: {config.parameters.deploy ? "enabled" : "disabled"}
108
+ </div>
109
+ </div>
98
110
  <div>
99
- {config.machineIds.length} configured {failingMachines.length > 0 && `(${failingMachines.length} failing)`} {missingMachines.length > 0 && `(${missingMachines.length} machine hasn't run service yet)`} {unknown > 0 && `(${unknown} unknown)`} • {totalLaunches} launches •
100
- Deploy: {config.parameters.deploy ? "enabled" : "disabled"}
111
+ Updated {formatTimeJSX(config.info.lastUpdatedTime)}
101
112
  </div>
102
113
  </div>
103
- <div>
104
- Updated {formatVeryNiceDateTime(config.info.lastUpdatedTime)}
105
- </div>
106
- </div>
107
- </div>;
114
+ </Anchor>
115
+ <UpdateServiceButtons service={config} />
116
+ </div>
117
+ ;
108
118
  })}
109
119
  </div>
110
120
  </div>;