@kwirthmagnify/kwirth-homepage-matrix 0.1.1

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.
Files changed (2) hide show
  1. package/front.js +355 -0
  2. package/package.json +7 -0
package/front.js ADDED
@@ -0,0 +1,355 @@
1
+ (() => {
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // kwirth-globals:react
29
+ var require_react = __commonJS({
30
+ "kwirth-globals:react"(exports, module) {
31
+ var _m = window.__kwirth__.React;
32
+ var _d = _m != null && "default" in Object(_m) ? _m.default : _m;
33
+ if (typeof _d !== "function" && _d != null && typeof _d.default !== "undefined") _d = _d.default;
34
+ module.exports = Object.assign({}, typeof _m === "object" && _m !== null ? _m : {}, { default: _d, __esModule: true });
35
+ }
36
+ });
37
+
38
+ // kwirth-globals:@mui/material
39
+ var require_material = __commonJS({
40
+ "kwirth-globals:@mui/material"(exports, module) {
41
+ var _m = window.__kwirth__.MUI.material;
42
+ var _d = _m != null && "default" in Object(_m) ? _m.default : _m;
43
+ if (typeof _d !== "function" && _d != null && typeof _d.default !== "undefined") _d = _d.default;
44
+ module.exports = Object.assign({}, typeof _m === "object" && _m !== null ? _m : {}, { default: _d, __esModule: true });
45
+ }
46
+ });
47
+
48
+ // kwirth-globals:@mui/icons-material
49
+ var require_icons_material = __commonJS({
50
+ "kwirth-globals:@mui/icons-material"(exports, module) {
51
+ var _m = window.__kwirth__.MUI.icons;
52
+ var _d = _m != null && "default" in Object(_m) ? _m.default : _m;
53
+ if (typeof _d !== "function" && _d != null && typeof _d.default !== "undefined") _d = _d.default;
54
+ module.exports = Object.assign({}, typeof _m === "object" && _m !== null ? _m : {}, { default: _d, __esModule: true });
55
+ }
56
+ });
57
+
58
+ // src/front/Matrix.tsx
59
+ var import_react = __toESM(require_react(), 1);
60
+ var import_material = __toESM(require_material(), 1);
61
+ var import_icons_material = __toESM(require_icons_material(), 1);
62
+ var MATRIX_GREEN = "#00ff41";
63
+ var MATRIX_DIM = "#006620";
64
+ var MATRIX_GLOW = "rgba(0,255,65,0.3)";
65
+ var EVENTS_LIMIT = 25;
66
+ var POLL_INTERVAL_MS = 1e4;
67
+ var MatrixRain = () => {
68
+ const canvasRef = (0, import_react.useRef)(null);
69
+ (0, import_react.useEffect)(() => {
70
+ const canvas = canvasRef.current;
71
+ if (!canvas) return;
72
+ const ctx = canvas.getContext("2d");
73
+ if (!ctx) return;
74
+ const fontSize = 22;
75
+ const chars = "\u30A2\u30A4\u30A6\u30A8\u30AA\u30AB\u30AD\u30AF\u30B1\u30B3\u30B5\u30B7\u30B9\u30BB\u30BD\u30BF\u30C1\u30C4\u30C6\u30C8\u30CA\u30CB\u30CC\u30CD\u30CE\u30CF\u30D2\u30D5\u30D8\u30DB\u30DE\u30DF\u30E0\u30E1\u30E2\u30E4\u30E6\u30E8\u30E9\u30EA\u30EB\u30EC\u30ED\u30EF\u30F2\u30F30123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
76
+ let drops = [];
77
+ let animId;
78
+ let frame = 0;
79
+ const SPEED = 12;
80
+ const resize = (w, h) => {
81
+ canvas.width = w;
82
+ canvas.height = h;
83
+ const cols = Math.floor(w / fontSize);
84
+ drops = Array.from({ length: cols }, () => -Math.floor(Math.random() * (h / fontSize) * 4));
85
+ };
86
+ const draw = () => {
87
+ frame++;
88
+ ctx.fillStyle = "rgba(0,0,0,0.02)";
89
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
90
+ ctx.font = `${fontSize}px monospace`;
91
+ if (frame % SPEED === 0) {
92
+ for (let i = 0; i < drops.length; i++) {
93
+ if (drops[i] > 0) {
94
+ const bright = Math.random() > 0.97;
95
+ ctx.fillStyle = bright ? "#ccffcc" : MATRIX_GREEN;
96
+ ctx.fillText(chars[Math.floor(Math.random() * chars.length)], i * fontSize, drops[i] * fontSize);
97
+ }
98
+ if (drops[i] * fontSize > canvas.height) {
99
+ drops[i] = -Math.floor(Math.random() * (canvas.height / fontSize) * 3 + canvas.height / fontSize * 2);
100
+ }
101
+ drops[i]++;
102
+ }
103
+ }
104
+ animId = requestAnimationFrame(draw);
105
+ };
106
+ const observer = new ResizeObserver((entries) => {
107
+ const { width, height } = entries[0].contentRect;
108
+ resize(Math.round(width), Math.round(height));
109
+ });
110
+ observer.observe(canvas);
111
+ animId = requestAnimationFrame(draw);
112
+ return () => {
113
+ cancelAnimationFrame(animId);
114
+ observer.disconnect();
115
+ };
116
+ }, []);
117
+ return /* @__PURE__ */ import_react.default.createElement(
118
+ "canvas",
119
+ {
120
+ ref: canvasRef,
121
+ style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "block" }
122
+ }
123
+ );
124
+ };
125
+ var StatusLight = ({ online, events, animationDelay }) => {
126
+ const hasError = online && (events ?? []).some((e) => e.type !== "Normal" && e.type !== "Warning");
127
+ const hasWarning = online && !hasError && (events ?? []).some((e) => e.type === "Warning");
128
+ const color = !online ? "#ff3333" : hasError ? "#ff3333" : hasWarning ? "#ff9800" : MATRIX_GREEN;
129
+ const label = !online ? "Offline" : hasError ? "Errors detected" : hasWarning ? "Warnings detected" : "Online";
130
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: label }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: {
131
+ width: 12,
132
+ height: 12,
133
+ borderRadius: "50%",
134
+ bgcolor: color,
135
+ boxShadow: `0 0 6px 2px ${color}`,
136
+ transition: "all 0.3s ease",
137
+ cursor: "default",
138
+ animation: online && !hasError && !hasWarning ? "matrix-status-pulse 2.5s ease-in-out infinite" : "none",
139
+ animationDelay: animationDelay ?? "0s"
140
+ } }));
141
+ };
142
+ var MetricBar = ({ label, value }) => {
143
+ const barRef = (0, import_react.useRef)(null);
144
+ const [cols, setCols] = (0, import_react.useState)(10);
145
+ (0, import_react.useEffect)(() => {
146
+ if (!barRef.current) return;
147
+ const obs = new ResizeObserver(() => {
148
+ if (barRef.current) setCols(Math.max(5, Math.floor(barRef.current.offsetWidth / 6)));
149
+ });
150
+ obs.observe(barRef.current);
151
+ return () => obs.disconnect();
152
+ }, []);
153
+ const filled = Math.round(value * cols / 100);
154
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(cols - filled);
155
+ const color = value > 90 ? "#ff3333" : value > 80 ? "#b36200" : MATRIX_GREEN;
156
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", alignItems: "center", spacing: 0 }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { sx: { fontFamily: "monospace", fontSize: "0.65rem", color: MATRIX_DIM, width: 28, flexShrink: 0 } }, label), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { ref: barRef, sx: { fontFamily: "monospace", fontSize: "0.65rem", color, letterSpacing: "-0.5px", flex: 1, overflow: "hidden", whiteSpace: "nowrap" } }, bar), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { sx: { fontFamily: "monospace", fontSize: "0.65rem", color, flexShrink: 0, pl: 0.5, width: 32, textAlign: "right" } }, Math.round(value), "%"));
157
+ };
158
+ var MiniInfoCard = ({ label, value, stretch }) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: {
159
+ border: `1px solid ${MATRIX_DIM}`,
160
+ borderRadius: 1,
161
+ px: 1,
162
+ display: "flex",
163
+ flexDirection: "column",
164
+ alignItems: "center",
165
+ justifyContent: "center",
166
+ minWidth: 70,
167
+ ...stretch ? { height: "100%" } : { py: 0.75 }
168
+ } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { sx: { fontFamily: "monospace", fontSize: "1rem", color: MATRIX_GREEN, lineHeight: 1.2 } }, value), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { sx: { fontFamily: "monospace", fontSize: "0.78rem", color: MATRIX_DIM, lineHeight: 1.2 } }, label));
169
+ var EventLog = ({ events }) => {
170
+ const boxRef = (0, import_react.useRef)(null);
171
+ (0, import_react.useEffect)(() => {
172
+ if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight;
173
+ }, [events]);
174
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { ref: boxRef, sx: {
175
+ flex: 1,
176
+ overflowY: "auto",
177
+ overflowX: "auto",
178
+ fontFamily: "monospace",
179
+ fontSize: "0.55rem",
180
+ lineHeight: 1.9,
181
+ color: MATRIX_DIM,
182
+ bgcolor: "rgba(0,18,6,0.92)",
183
+ border: `1px solid ${MATRIX_DIM}`,
184
+ borderRadius: 1,
185
+ p: 1,
186
+ whiteSpace: "nowrap",
187
+ "&::-webkit-scrollbar": { width: 4, height: 4 },
188
+ "&::-webkit-scrollbar-thumb": { bgcolor: MATRIX_DIM, borderRadius: 2 }
189
+ } }, events.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { color: MATRIX_GREEN, opacity: 0.4 } }, "// follow the white rabbit") : [...events].reverse().map((e, i) => {
190
+ const d = new Date(e.time);
191
+ const t = [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
192
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: i, sx: { color: e.type === "Warning" ? "#ff9800" : MATRIX_GREEN, mb: 0.2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, { component: "span", sx: { color: MATRIX_DIM, mr: 0.5 } }, t), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { component: "span", sx: { mr: 0.5 } }, "[", e.reason, "]"), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { component: "span", sx: { color: MATRIX_DIM, mr: 0.5 } }, e.object), e.message);
193
+ }));
194
+ };
195
+ var Matrix = (props) => {
196
+ (0, import_react.useEffect)(() => {
197
+ const id = "matrix-keyframes";
198
+ if (!document.getElementById(id)) {
199
+ const style = document.createElement("style");
200
+ style.id = id;
201
+ style.textContent = `@keyframes matrix-status-pulse {
202
+ 0%, 100% { opacity: 0.25; box-shadow: none; }
203
+ 50% { opacity: 0.85; box-shadow: 0 0 7px 2px ${MATRIX_GREEN}; }
204
+ }`;
205
+ document.head.appendChild(style);
206
+ }
207
+ return () => {
208
+ document.getElementById("matrix-keyframes")?.remove();
209
+ };
210
+ }, []);
211
+ const containerRef = (0, import_react.useRef)(null);
212
+ const [containerHeight, setContainerHeight] = import_react.default.useState(0);
213
+ (0, import_react.useEffect)(() => {
214
+ const observer = new ResizeObserver(() => {
215
+ if (!containerRef.current) return;
216
+ const { top } = containerRef.current.getBoundingClientRect();
217
+ setContainerHeight(window.innerHeight - top);
218
+ });
219
+ observer.observe(document.body);
220
+ return () => observer.disconnect();
221
+ }, [containerRef.current]);
222
+ const [clusterMetrics, setClusterMetrics] = (0, import_react.useState)({});
223
+ (0, import_react.useEffect)(() => {
224
+ if (!props.getClusterMetrics) return;
225
+ const fetchAll = () => {
226
+ props.clusters.forEach((cluster) => {
227
+ props.getClusterMetrics(cluster.name).then((m) => {
228
+ if (m) setClusterMetrics((prev) => ({ ...prev, [cluster.name]: m }));
229
+ });
230
+ });
231
+ };
232
+ fetchAll();
233
+ const timer = setInterval(fetchAll, POLL_INTERVAL_MS);
234
+ return () => clearInterval(timer);
235
+ }, [props.clusters, props.getClusterMetrics]);
236
+ const [clusterEvents, setClusterEvents] = (0, import_react.useState)({});
237
+ (0, import_react.useEffect)(() => {
238
+ if (!props.getClusterEvents) return;
239
+ const fetchAll = () => {
240
+ props.clusters.forEach((cluster) => {
241
+ props.getClusterEvents(cluster.name, EVENTS_LIMIT).then((events) => {
242
+ setClusterEvents((prev) => ({ ...prev, [cluster.name]: events }));
243
+ });
244
+ });
245
+ };
246
+ fetchAll();
247
+ const timer = setInterval(fetchAll, POLL_INTERVAL_MS);
248
+ return () => clearInterval(timer);
249
+ }, [props.clusters, props.getClusterEvents]);
250
+ const launchMagnify = (clusterName) => {
251
+ props.onHomepageSelectTab({
252
+ name: clusterName,
253
+ description: "",
254
+ channel: "magnify",
255
+ channelObject: { clusterName, view: "cluster", namespace: "", group: "", pod: "", container: "" }
256
+ });
257
+ };
258
+ const launchTopology = (clusterName) => {
259
+ props.onHomepageSelectTab({
260
+ name: clusterName,
261
+ description: "",
262
+ channel: "topology",
263
+ channelObject: { clusterName, view: "cluster", namespace: "", group: "", pod: "", container: "" }
264
+ });
265
+ };
266
+ const hasMagnify = props.frontChannels.has("magnify");
267
+ const hasTopology = props.frontChannels.has("topology");
268
+ const magnifyClass = props.frontChannels.get("magnify");
269
+ const magnifyIcon = magnifyClass ? new magnifyClass().getChannelIcon() : null;
270
+ const topologyClass = props.frontChannels.get("topology");
271
+ const topologyIcon = topologyClass ? new topologyClass().getChannelIcon() : /* @__PURE__ */ import_react.default.createElement(import_icons_material.AccountTree, null);
272
+ const matrixButtonSx = {
273
+ fontFamily: "monospace",
274
+ "&:not(.Mui-disabled)": { color: MATRIX_GREEN, borderColor: MATRIX_GREEN },
275
+ "&:hover:not(.Mui-disabled)": {
276
+ borderColor: MATRIX_GREEN,
277
+ bgcolor: "rgba(0,255,65,0.1)",
278
+ boxShadow: `0 0 8px ${MATRIX_GLOW}`
279
+ }
280
+ };
281
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { ref: containerRef, sx: { position: "relative", width: "100%", height: `${containerHeight}px`, overflow: "hidden", bgcolor: "#000" } }, /* @__PURE__ */ import_react.default.createElement(MatrixRain, null), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: {
282
+ position: "relative",
283
+ zIndex: 1,
284
+ p: 3,
285
+ height: "100%",
286
+ overflowY: "auto",
287
+ display: "grid",
288
+ gridTemplateColumns: "repeat(2, 1fr)",
289
+ gap: 4,
290
+ alignContent: "start"
291
+ } }, props.clusters.map((cluster, idx) => {
292
+ const clusterChannelIds = new Set((cluster.kwirthData?.channels ?? []).map((ch) => ch.id));
293
+ const clusterHasMagnify = hasMagnify && clusterChannelIds.has("magnify");
294
+ const clusterHasTopology = hasTopology && clusterChannelIds.has("topology");
295
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Card, { key: cluster.name, variant: "outlined", sx: {
296
+ bgcolor: "rgba(0,0,0,0.80)",
297
+ border: `1px solid ${MATRIX_GREEN}`,
298
+ boxShadow: `0 0 10px ${MATRIX_GLOW}, inset 0 0 10px rgba(0,255,65,0.05)`,
299
+ color: MATRIX_GREEN,
300
+ backdropFilter: "blur(2px)",
301
+ height: `${Math.floor((containerHeight - 24 * 2 - 24) / 2) - 4}px`,
302
+ overflow: "hidden"
303
+ } }, /* @__PURE__ */ import_react.default.createElement(import_material.CardContent, { sx: { height: "100%", display: "flex", flexDirection: "row", gap: 2, py: 2, "&:last-child": { pb: 2 } } }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "column", justifyContent: "space-between", sx: { flex: "0 0 40%", minWidth: 0 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "column", spacing: 1 }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", alignItems: "center", spacing: 1 }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "h6", sx: {
304
+ fontFamily: "monospace",
305
+ color: MATRIX_GREEN,
306
+ lineHeight: 1.3,
307
+ textShadow: `0 0 8px ${MATRIX_GREEN}`,
308
+ overflow: "hidden",
309
+ textOverflow: "ellipsis",
310
+ whiteSpace: "nowrap",
311
+ m: 0
312
+ } }, cluster.name), /* @__PURE__ */ import_react.default.createElement(StatusLight, { online: !!clusterMetrics[cluster.name], events: clusterEvents[cluster.name], animationDelay: `${idx * 0.7 % 2.5}s` })), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", sx: {
313
+ fontFamily: "monospace",
314
+ color: MATRIX_DIM,
315
+ overflow: "hidden",
316
+ textOverflow: "ellipsis",
317
+ whiteSpace: "nowrap"
318
+ } }, cluster.url), clusterMetrics[cluster.name] && /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "column", spacing: 0.25 }, /* @__PURE__ */ import_react.default.createElement(MetricBar, { label: "CPU", value: clusterMetrics[cluster.name].cpu }), /* @__PURE__ */ import_react.default.createElement(MetricBar, { label: "MEM", value: clusterMetrics[cluster.name].memory }), /* @__PURE__ */ import_react.default.createElement(MetricBar, { label: "POD", value: clusterMetrics[cluster.name].maxPods > 0 ? clusterMetrics[cluster.name].pods / clusterMetrics[cluster.name].maxPods * 100 : 0 }))), clusterMetrics[cluster.name] && /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1.5, sx: { justifyContent: "center" } }, /* @__PURE__ */ import_react.default.createElement(MiniInfoCard, { label: "vCPUs", value: clusterMetrics[cluster.name].vcpus != null ? String(clusterMetrics[cluster.name].vcpus) : "--" }), /* @__PURE__ */ import_react.default.createElement(MiniInfoCard, { label: "RAM", value: clusterMetrics[cluster.name].totalMemoryBytes != null ? `${(clusterMetrics[cluster.name].totalMemoryBytes / 1073741824).toFixed(0)}G` : "--" }), /* @__PURE__ */ import_react.default.createElement(MiniInfoCard, { label: "Pods", value: clusterMetrics[cluster.name].pods ? String(clusterMetrics[cluster.name].pods) : "--" })), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "column", spacing: 0.75 }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0.5, alignItems: "center" }, (cluster.kwirthData?.channels ?? []).map((ch) => {
319
+ const channelClass = props.frontChannels.get(ch.id);
320
+ if (!channelClass) return null;
321
+ const icon = new channelClass().getChannelIcon();
322
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { key: ch.id, title: ch.id }, import_react.default.cloneElement(icon, { fontSize: "small", sx: { color: MATRIX_GREEN, opacity: 0.6 } }));
323
+ })), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, justifyContent: "flex-start" }, /* @__PURE__ */ import_react.default.createElement(
324
+ import_material.Button,
325
+ {
326
+ variant: "outlined",
327
+ size: "small",
328
+ disabled: !clusterHasTopology,
329
+ startIcon: topologyIcon,
330
+ onClick: () => launchTopology(cluster.name),
331
+ sx: matrixButtonSx
332
+ },
333
+ "Topology"
334
+ ), /* @__PURE__ */ import_react.default.createElement(
335
+ import_material.Button,
336
+ {
337
+ variant: "outlined",
338
+ size: "small",
339
+ disabled: !clusterHasMagnify,
340
+ startIcon: magnifyIcon,
341
+ onClick: () => launchMagnify(cluster.name),
342
+ sx: matrixButtonSx
343
+ },
344
+ "Magnify"
345
+ )))), /* @__PURE__ */ import_react.default.createElement(EventLog, { events: clusterEvents[cluster.name] ?? [] })));
346
+ }), props.clusters.length === 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { sx: { fontFamily: "monospace", color: MATRIX_GREEN, p: 2, textShadow: `0 0 8px ${MATRIX_GREEN}` } }, "// Entering the matrix...")));
347
+ };
348
+
349
+ // src/front/index.ts
350
+ window.__kwirth_homepages__["matrix"] = {
351
+ homepageId: "matrix",
352
+ displayName: "Matrix",
353
+ Component: Matrix
354
+ };
355
+ })();
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "matrix",
3
+ "name": "@kwirthmagnify/kwirth-homepage-matrix",
4
+ "displayName": "Matrix",
5
+ "version": "0.1.1",
6
+ "description": "Matrix-style cluster overview homepage for Kwirth"
7
+ }