@dong-victor/dsh-better-sidebar-starter 0.1.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/README.md +55 -0
- package/cordis.patch.yml +14 -0
- package/dsh.plugin.json +16 -0
- package/lib/client.js +2465 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1030 -0
- package/package.json +82 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2465 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dong-victor/dsh-better-sidebar-starter",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/client/icons.tsx
|
|
9
|
+
/**
|
|
10
|
+
* SVG icons for the starter plugin. Each icon is a React component that
|
|
11
|
+
* takes a size prop and renders an inline SVG. Monochrome, currentColor fill.
|
|
12
|
+
* @module dsh-better-sidebar-starter/client/icons
|
|
13
|
+
*/
|
|
14
|
+
function svg(size, pathChildren) {
|
|
15
|
+
const svgHtml = `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">${pathChildren}</svg>`;
|
|
16
|
+
return (0, react.createElement)("span", {
|
|
17
|
+
style: {
|
|
18
|
+
display: "inline-flex",
|
|
19
|
+
alignItems: "center",
|
|
20
|
+
justifyContent: "center",
|
|
21
|
+
width: size,
|
|
22
|
+
height: size
|
|
23
|
+
},
|
|
24
|
+
dangerouslySetInnerHTML: { __html: svgHtml }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** 🚀 Rocket — the tab icon. */
|
|
28
|
+
function RocketIcon({ size }) {
|
|
29
|
+
return svg(size, `<path d="M9.19 8.35c.34-.05.67-.1 1-.13 1.2-1.49 2.85-2.49 4.85-3.02 2-.53 4.16-.42 6.5.33.75 2.34.86 4.5.33 6.5-.53 2-1.53 3.65-3.02 4.85-.04.33-.08.66-.13 1-.13.87-.32 1.71-.57 2.5-.42 1.3-1.02 2.5-1.78 3.6-.42.6-1.1.96-1.86.96-.72 0-1.4-.34-1.84-.93l-2.06-2.76c-.2-.27-.5-.43-.82-.43H7.68c-.32 0-.62-.16-.82-.43l-2.06-2.76C4.36 19.34 4 18.66 4 17.94c0-.76.36-1.44.96-1.86 1.1-.76 2.3-1.36 3.6-1.78.79-.25 1.63-.44 2.5-.57l.13-1c.05-.34.1-.67.13-1z" fill="currentColor" opacity="0.85"/><circle cx="15.5" cy="8.5" r="1.5" fill="currentColor" opacity="0.6"/><path d="M5 14c-1 0-2 .5-2 2 0 1 .5 2 .5 2s1-.5 1.5-.5 1.5.5 1.5.5 1-.5 1-2-.5-2-2-2z" fill="currentColor" opacity="0.5"/>`);
|
|
30
|
+
}
|
|
31
|
+
/** ▶ Play — start button. */
|
|
32
|
+
function PlayIcon({ size }) {
|
|
33
|
+
return svg(size, `<path d="M8 5v14l11-7z" fill="currentColor"/>`);
|
|
34
|
+
}
|
|
35
|
+
/** ⏹ Stop — stop button. */
|
|
36
|
+
function StopIcon({ size }) {
|
|
37
|
+
return svg(size, `<rect x="6" y="6" width="12" height="12" rx="1" fill="currentColor"/>`);
|
|
38
|
+
}
|
|
39
|
+
/** 🔄 Restart — restart button. */
|
|
40
|
+
function RestartIcon({ size }) {
|
|
41
|
+
return svg(size, `<path d="M12 5V2L7 7l5 5V8c2.76 0 5 2.24 5 5s-2.24 5-5 5-5-2.24-5-5H5c0 3.87 3.13 7 7 7s7-3.13 7-7-3.13-7-7-7z" fill="currentColor"/>`);
|
|
42
|
+
}
|
|
43
|
+
/** ⚙ Gear — edit button. */
|
|
44
|
+
function GearIcon({ size }) {
|
|
45
|
+
return svg(size, `<path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3l-1.97-.39c-.11-.37-.26-.72-.44-1.06l1.14-1.64c.26-.37.22-.88-.09-1.2l-1.28-1.28c-.32-.31-.83-.35-1.2-.09l-1.64 1.14c-.34-.18-.69-.33-1.06-.44L14.94 4c-.1-.42-.47-.72-.9-.72h-1.8c-.43 0-.8.3-.9.72l-.39 1.97c-.37.11-.72.26-1.06.44L7.25 5.27c-.37-.26-.88-.22-1.2.09L4.77 6.64c-.31.32-.35.83-.09 1.2l1.14 1.64c-.18.34-.33.69-.44 1.06L3.4 11c-.42.1-.72.47-.72.9v1.8c0 .43.3.8.72.9l1.97.39c.11.37.26.72.44 1.06l-1.14 1.64c-.26.37-.22.88.09 1.2l1.28 1.28c.32.31.83.35 1.2.09l1.64-1.14c.34.18.69.33 1.06.44l.39 1.97c.1.42.47.72.9.72h1.8c.43 0 .8-.3.9-.72l.39-1.97c.37-.11.72-.26 1.06-.44l1.64 1.14c.37.26.88.22 1.2-.09l1.28-1.28c.31-.32.35-.83.09-1.2l-1.14-1.64c.18-.34.33-.69.44-1.06L20.6 14c.42-.1.72-.47.72-.9v-1.8c0-.43-.3-.8-.72-.9z" fill="currentColor"/>`);
|
|
46
|
+
}
|
|
47
|
+
/** 🗑 Trash — delete button. */
|
|
48
|
+
function TrashIcon({ size }) {
|
|
49
|
+
return svg(size, `<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>`);
|
|
50
|
+
}
|
|
51
|
+
/** ➕ Plus — add new config. */
|
|
52
|
+
function PlusIcon({ size }) {
|
|
53
|
+
return svg(size, `<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>`);
|
|
54
|
+
}
|
|
55
|
+
/** ✕ Close — modal cancel. */
|
|
56
|
+
function CloseIcon({ size }) {
|
|
57
|
+
return svg(size, `<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>`);
|
|
58
|
+
}
|
|
59
|
+
/** ▾ Chevron down — tree expand. */
|
|
60
|
+
function ChevronDownIcon({ size }) {
|
|
61
|
+
return svg(size, `<path d="M7 10l5 5 5-5z" fill="currentColor"/>`);
|
|
62
|
+
}
|
|
63
|
+
/** ▸ Chevron right — tree collapse. */
|
|
64
|
+
function ChevronRightIcon({ size }) {
|
|
65
|
+
return svg(size, `<path d="M10 7l5 5-5 5z" fill="currentColor"/>`);
|
|
66
|
+
}
|
|
67
|
+
/** 🟢 Dot — running status. */
|
|
68
|
+
function RunningDotIcon({ size }) {
|
|
69
|
+
return svg(size, `<circle cx="12" cy="12" r="5" fill="#4e9a06"/>`);
|
|
70
|
+
}
|
|
71
|
+
/** ✅ Check — completed status. */
|
|
72
|
+
function CheckIcon({ size }) {
|
|
73
|
+
return svg(size, `<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="#4e9a06"/>`);
|
|
74
|
+
}
|
|
75
|
+
/** 🔴 Error dot — failed status. */
|
|
76
|
+
function ErrorDotIcon({ size }) {
|
|
77
|
+
return svg(size, `<circle cx="12" cy="12" r="5" fill="#cc0000"/>`);
|
|
78
|
+
}
|
|
79
|
+
/** 📦 npm package icon. */
|
|
80
|
+
function NpmIcon({ size }) {
|
|
81
|
+
return svg(size, `<rect x="3" y="3" width="18" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M7 17V8h4v6h2V8h4v9h-4v-3h-2v3H7z" fill="currentColor"/>`);
|
|
82
|
+
}
|
|
83
|
+
/** ☕ springboot icon. */
|
|
84
|
+
function SpringIcon({ size }) {
|
|
85
|
+
return svg(size, `<path d="M12 2C7.5 2 4 5.5 4 10c0 3 2 5.5 4.5 7 .5-2 1.5-3.5 3-4.5 1.5 1 2.5 2.5 3 4.5 2.5-1.5 4.5-4 4.5-7 0-4.5-3.5-8-7-8z" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 10c1-1 2.5-1.5 4-1.5s3 .5 4 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`);
|
|
86
|
+
}
|
|
87
|
+
/** 🐍 python icon. */
|
|
88
|
+
function PythonIcon({ size }) {
|
|
89
|
+
return svg(size, `<path d="M12 4c-2 0-4 1-4 3v2h4v1H6c-1 0-2 1-2 3s1 3 2 3h2v-2c0-2 2-3 4-3h2c1 0 2-1 2-2V7c0-2-2-3-4-3z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="10.5" cy="6.5" r="0.8" fill="currentColor"/><path d="M12 20c2 0 4-1 4-3v-2h-4v-1h4c1 0 2-1 2-3s-1-3-2-3h-2v2c0 2-2 3-4 3h-2c-1 0-2 1-2 2v3c0 2 2 3 4 3z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="13.5" cy="17.5" r="0.8" fill="currentColor"/>`);
|
|
90
|
+
}
|
|
91
|
+
/** ⚡ custom icon. */
|
|
92
|
+
function CustomIcon({ size }) {
|
|
93
|
+
return svg(size, `<path d="M11 21h-1l1-7H7.5c-.6 0-.8-.4-.5-.9 1.5-2.6 3.7-6.2 6.5-10.1h1l-1 7h3.5c.6 0 .8.4.5.9C14.5 14.7 12.3 18.1 11 21z" fill="currentColor"/>`);
|
|
94
|
+
}
|
|
95
|
+
/** 📁 Folder — directory tree icon. */
|
|
96
|
+
function FolderIcon({ size }) {
|
|
97
|
+
return svg(size, `<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" fill="currentColor" opacity="0.7"/>`);
|
|
98
|
+
}
|
|
99
|
+
/** 📋 Clipboard — bulk paste env. */
|
|
100
|
+
function ClipboardIcon({ size }) {
|
|
101
|
+
return svg(size, `<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" fill="currentColor"/>`);
|
|
102
|
+
}
|
|
103
|
+
/** Get the icon for a config type. */
|
|
104
|
+
function typeIcon(type, size) {
|
|
105
|
+
switch (type) {
|
|
106
|
+
case "npm": return NpmIcon({ size });
|
|
107
|
+
case "springboot": return SpringIcon({ size });
|
|
108
|
+
case "python": return PythonIcon({ size });
|
|
109
|
+
default: return CustomIcon({ size });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/client/ConfigTree.tsx
|
|
114
|
+
/**
|
|
115
|
+
* Config tree: the left pane showing all run configurations grouped by type,
|
|
116
|
+
* with running instances as child nodes.
|
|
117
|
+
* @module dsh-better-sidebar-starter/client/ConfigTree
|
|
118
|
+
*/
|
|
119
|
+
/** Type group order. */
|
|
120
|
+
const TYPE_ORDER = [
|
|
121
|
+
"npm",
|
|
122
|
+
"springboot",
|
|
123
|
+
"python",
|
|
124
|
+
"custom"
|
|
125
|
+
];
|
|
126
|
+
const TYPE_LABELS$2 = {
|
|
127
|
+
npm: "npm",
|
|
128
|
+
springboot: "Spring Boot",
|
|
129
|
+
python: "Python",
|
|
130
|
+
custom: "Custom"
|
|
131
|
+
};
|
|
132
|
+
/** Status icon for an instance. */
|
|
133
|
+
function statusIcon(status, size) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case "running": return RunningDotIcon({ size });
|
|
136
|
+
case "exited": return CheckIcon({ size });
|
|
137
|
+
case "killed":
|
|
138
|
+
case "error": return ErrorDotIcon({ size });
|
|
139
|
+
default: return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The config tree component. */
|
|
143
|
+
function ConfigTree(props) {
|
|
144
|
+
const { configs, instances, selectedInstanceId, selectedConfigId, onSelectConfig, onSelectInstance, onStartConfig, onStopInstance, onEditConfig } = props;
|
|
145
|
+
const [collapsed, setCollapsed] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
146
|
+
const toggleGroup = (type) => {
|
|
147
|
+
setCollapsed((prev) => {
|
|
148
|
+
const next = new Set(prev);
|
|
149
|
+
if (next.has(type)) next.delete(type);
|
|
150
|
+
else next.add(type);
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const groups = (0, react.useMemo)(() => {
|
|
155
|
+
const map = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const config of configs) {
|
|
157
|
+
const arr = map.get(config.type) ?? [];
|
|
158
|
+
arr.push(config);
|
|
159
|
+
map.set(config.type, arr);
|
|
160
|
+
}
|
|
161
|
+
for (const arr of map.values()) arr.sort((a, b) => (b.lastRunAt ?? 0) - (a.lastRunAt ?? 0));
|
|
162
|
+
return TYPE_ORDER.filter((type) => map.has(type) && map.get(type).length > 0).map((type) => ({
|
|
163
|
+
type,
|
|
164
|
+
items: map.get(type)
|
|
165
|
+
}));
|
|
166
|
+
}, [configs]);
|
|
167
|
+
if (configs.length === 0) return (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置", (0, react.createElement)("br"), "点击右上角 + 新建配置");
|
|
168
|
+
return (0, react.createElement)("div", { className: "sts-tree" }, ...groups.map((group) => {
|
|
169
|
+
const isCollapsed = collapsed.has(group.type);
|
|
170
|
+
const label = TYPE_LABELS$2[group.type] ?? group.type;
|
|
171
|
+
const runningCount = instances.filter((inst) => inst.configType === group.type && inst.status === "running").length;
|
|
172
|
+
return (0, react.createElement)("div", { key: group.type }, (0, react.createElement)("div", {
|
|
173
|
+
className: "sts-group-header",
|
|
174
|
+
onClick: () => toggleGroup(group.type)
|
|
175
|
+
}, isCollapsed ? (0, react.createElement)(ChevronRightIcon, { size: 12 }) : (0, react.createElement)(ChevronDownIcon, { size: 12 }), typeIcon(group.type, 14), label, (0, react.createElement)("span", { className: "sts-group-count" }, ` (${group.items.length}${runningCount > 0 ? `, ${runningCount} 运行中` : ""})`)), (0, react.createElement)("div", { className: `sts-group-children${isCollapsed ? " collapsed" : ""}` }, ...group.items.flatMap((config) => {
|
|
176
|
+
const configInstances = instances.filter((inst) => inst.configId === config.id);
|
|
177
|
+
const isRunning = configInstances.some((inst) => inst.status === "running");
|
|
178
|
+
const isSelected = selectedConfigId === config.id && selectedInstanceId === null;
|
|
179
|
+
const rows = [(0, react.createElement)("div", {
|
|
180
|
+
key: config.id,
|
|
181
|
+
className: `sts-config-row${isSelected ? " selected" : ""}${isRunning ? " running" : ""}`,
|
|
182
|
+
onClick: () => onSelectConfig(config.id)
|
|
183
|
+
}, isRunning ? RunningDotIcon({ size: 12 }) : typeIcon(config.type, 12), (0, react.createElement)("div", { className: "sts-config-name" }, config.name), (0, react.createElement)("div", { className: "sts-config-cmd" }, config.command), (0, react.createElement)("div", { className: "sts-config-actions" }, isRunning ? (0, react.createElement)("button", {
|
|
184
|
+
className: "sts-icon-btn stop",
|
|
185
|
+
title: "停止全部",
|
|
186
|
+
onClick: (e) => {
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
for (const inst of configInstances) if (inst.status === "running") onStopInstance(inst.id);
|
|
189
|
+
}
|
|
190
|
+
}, StopIcon({ size: 14 })) : (0, react.createElement)("button", {
|
|
191
|
+
className: "sts-icon-btn play",
|
|
192
|
+
title: "启动",
|
|
193
|
+
onClick: (e) => {
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
onStartConfig(config.id);
|
|
196
|
+
}
|
|
197
|
+
}, PlayIcon({ size: 14 })), (0, react.createElement)("button", {
|
|
198
|
+
className: "sts-icon-btn gear",
|
|
199
|
+
title: "编辑",
|
|
200
|
+
onClick: (e) => {
|
|
201
|
+
e.stopPropagation();
|
|
202
|
+
onEditConfig(config.id);
|
|
203
|
+
}
|
|
204
|
+
}, GearIcon({ size: 12 }))))];
|
|
205
|
+
for (const inst of configInstances) {
|
|
206
|
+
const instSelected = selectedInstanceId === inst.id;
|
|
207
|
+
rows.push((0, react.createElement)("div", {
|
|
208
|
+
key: inst.id,
|
|
209
|
+
className: `sts-instance-row${instSelected ? " selected" : ""}`,
|
|
210
|
+
onClick: () => onSelectInstance(inst.id)
|
|
211
|
+
}, statusIcon(inst.status, 10), `#${inst.id.slice(0, 8)}`, (0, react.createElement)("span", { className: "sts-instance-pid" }, inst.status === "running" ? ` pid:${inst.pid}` : ` ${inst.status}`), inst.status === "running" ? (0, react.createElement)("button", {
|
|
212
|
+
className: "sts-icon-btn stop",
|
|
213
|
+
title: "停止",
|
|
214
|
+
onClick: (e) => {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
onStopInstance(inst.id);
|
|
217
|
+
}
|
|
218
|
+
}, StopIcon({ size: 12 })) : null));
|
|
219
|
+
}
|
|
220
|
+
return rows;
|
|
221
|
+
})));
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/client/ansi.ts
|
|
226
|
+
function cloneState(s) {
|
|
227
|
+
return {
|
|
228
|
+
text: "",
|
|
229
|
+
bold: s.bold,
|
|
230
|
+
dim: s.dim,
|
|
231
|
+
italic: s.italic,
|
|
232
|
+
underline: s.underline,
|
|
233
|
+
fg: s.fg,
|
|
234
|
+
bg: s.bg
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Apply one SGR parameter to the parse state. */
|
|
238
|
+
function applySgr(state, code) {
|
|
239
|
+
if (code === 0) {
|
|
240
|
+
state.bold = false;
|
|
241
|
+
state.dim = false;
|
|
242
|
+
state.italic = false;
|
|
243
|
+
state.underline = false;
|
|
244
|
+
state.fg = null;
|
|
245
|
+
state.bg = null;
|
|
246
|
+
} else if (code === 1) state.bold = true;
|
|
247
|
+
else if (code === 2) state.dim = true;
|
|
248
|
+
else if (code === 3) state.italic = true;
|
|
249
|
+
else if (code === 4) state.underline = true;
|
|
250
|
+
else if (code === 22) {
|
|
251
|
+
state.bold = false;
|
|
252
|
+
state.dim = false;
|
|
253
|
+
} else if (code === 23) state.italic = false;
|
|
254
|
+
else if (code === 24) state.underline = false;
|
|
255
|
+
else if (code >= 30 && code <= 37) state.fg = code - 30;
|
|
256
|
+
else if (code === 39) state.fg = null;
|
|
257
|
+
else if (code >= 40 && code <= 47) state.bg = code - 40;
|
|
258
|
+
else if (code === 49) state.bg = null;
|
|
259
|
+
else if (code >= 90 && code <= 97) state.fg = code - 90 + 8;
|
|
260
|
+
else if (code >= 100 && code <= 107) state.bg = code - 100 + 8;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parse a string with ANSI escape codes into styled segments.
|
|
264
|
+
* Returns an array of AnsiSegment objects ready for React rendering.
|
|
265
|
+
*/
|
|
266
|
+
function parseAnsi(input) {
|
|
267
|
+
const state = {
|
|
268
|
+
bold: false,
|
|
269
|
+
dim: false,
|
|
270
|
+
italic: false,
|
|
271
|
+
underline: false,
|
|
272
|
+
fg: null,
|
|
273
|
+
bg: null
|
|
274
|
+
};
|
|
275
|
+
const segments = [];
|
|
276
|
+
let current = cloneState(state);
|
|
277
|
+
const regex = /\x1b\[([0-9;]*)m|\x1b\][^\x07]*\x07|\x1b\[[0-9;]*[A-Za-z]/g;
|
|
278
|
+
let lastIndex = 0;
|
|
279
|
+
let match;
|
|
280
|
+
while ((match = regex.exec(input)) !== null) {
|
|
281
|
+
if (match.index > lastIndex) current.text += input.slice(lastIndex, match.index);
|
|
282
|
+
if (match[1] !== void 0) {
|
|
283
|
+
if (current.text !== "") segments.push(current);
|
|
284
|
+
const params = match[1] === "" ? "0" : match[1];
|
|
285
|
+
for (const codeStr of params.split(";")) {
|
|
286
|
+
const code = codeStr === "" ? 0 : Number(codeStr);
|
|
287
|
+
if (!Number.isNaN(code)) applySgr(state, code);
|
|
288
|
+
}
|
|
289
|
+
current = cloneState(state);
|
|
290
|
+
}
|
|
291
|
+
lastIndex = regex.lastIndex;
|
|
292
|
+
}
|
|
293
|
+
if (lastIndex < input.length) current.text += input.slice(lastIndex);
|
|
294
|
+
if (current.text !== "") segments.push(current);
|
|
295
|
+
return segments;
|
|
296
|
+
}
|
|
297
|
+
/** Map an ANSI color index (0-15) to a CSS color string. */
|
|
298
|
+
const ANSI_COLORS = [
|
|
299
|
+
"#000000",
|
|
300
|
+
"#cc0000",
|
|
301
|
+
"#4e9a06",
|
|
302
|
+
"#c4a000",
|
|
303
|
+
"#3465a4",
|
|
304
|
+
"#75507b",
|
|
305
|
+
"#06989a",
|
|
306
|
+
"#d3d7cf",
|
|
307
|
+
"#555753",
|
|
308
|
+
"#ef2929",
|
|
309
|
+
"#8ae234",
|
|
310
|
+
"#fce94f",
|
|
311
|
+
"#729fcf",
|
|
312
|
+
"#ad7fa8",
|
|
313
|
+
"#34e2e2",
|
|
314
|
+
"#eeeeec"
|
|
315
|
+
];
|
|
316
|
+
/** Resolve a segment's foreground color to a CSS string, or null for default. */
|
|
317
|
+
function segmentFgColor(seg) {
|
|
318
|
+
if (seg.fg === null) return null;
|
|
319
|
+
return ANSI_COLORS[seg.fg] ?? null;
|
|
320
|
+
}
|
|
321
|
+
/** Resolve a segment's background color to a CSS string, or null for default. */
|
|
322
|
+
function segmentBgColor(seg) {
|
|
323
|
+
if (seg.bg === null) return null;
|
|
324
|
+
return ANSI_COLORS[seg.bg] ?? null;
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/client/LogView.tsx
|
|
328
|
+
/**
|
|
329
|
+
* Log view: the right pane showing real-time log output from a running
|
|
330
|
+
* instance. Connects to the WebSocket endpoint, parses ANSI colors, and
|
|
331
|
+
* renders log lines with auto-scroll.
|
|
332
|
+
* @module dsh-better-sidebar-starter/client/LogView
|
|
333
|
+
*/
|
|
334
|
+
/** Render ANSI-colored text segments. */
|
|
335
|
+
function renderAnsiLine(text, keyBase) {
|
|
336
|
+
return parseAnsi(text).map((seg, i) => {
|
|
337
|
+
const style = {};
|
|
338
|
+
const fg = segmentFgColor(seg);
|
|
339
|
+
const bg = segmentBgColor(seg);
|
|
340
|
+
if (fg !== null) style.color = fg;
|
|
341
|
+
if (bg !== null) style.backgroundColor = bg;
|
|
342
|
+
if (seg.bold) style.fontWeight = "bold";
|
|
343
|
+
if (seg.dim) style.opacity = "0.6";
|
|
344
|
+
if (seg.italic) style.fontStyle = "italic";
|
|
345
|
+
if (seg.underline) style.textDecoration = "underline";
|
|
346
|
+
return (0, react.createElement)("span", {
|
|
347
|
+
key: `${keyBase}-${i}`,
|
|
348
|
+
style
|
|
349
|
+
}, seg.text);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** The log view component. */
|
|
353
|
+
function LogView({ instance, onStop, onRestart }) {
|
|
354
|
+
const [logLines, setLogLines] = (0, react.useState)([]);
|
|
355
|
+
const [autoScroll, setAutoScroll] = (0, react.useState)(true);
|
|
356
|
+
const [exitInfo, setExitInfo] = (0, react.useState)(null);
|
|
357
|
+
const contentRef = (0, react.useRef)(null);
|
|
358
|
+
const wsRef = (0, react.useRef)(null);
|
|
359
|
+
const lineKeyCounter = (0, react.useRef)(0);
|
|
360
|
+
const lineBuffer = (0, react.useRef)({ current: [] });
|
|
361
|
+
/** Flush the buffer into a new log line. */
|
|
362
|
+
const flushLine = (0, react.useCallback)(() => {
|
|
363
|
+
const entries = lineBuffer.current.current;
|
|
364
|
+
if (entries.length === 0) return;
|
|
365
|
+
lineBuffer.current.current = [];
|
|
366
|
+
setLogLines((prev) => {
|
|
367
|
+
const key = lineKeyCounter.current++;
|
|
368
|
+
const next = [...prev, {
|
|
369
|
+
entries,
|
|
370
|
+
key
|
|
371
|
+
}];
|
|
372
|
+
if (next.length > 5e3) next.splice(0, next.length - 5e3);
|
|
373
|
+
return next;
|
|
374
|
+
});
|
|
375
|
+
}, []);
|
|
376
|
+
/** Add an entry to the current line buffer, splitting on newlines. */
|
|
377
|
+
const addEntry = (0, react.useCallback)((entry) => {
|
|
378
|
+
const parts = entry.text.split("\n");
|
|
379
|
+
for (let i = 0; i < parts.length; i++) {
|
|
380
|
+
if (i > 0) flushLine();
|
|
381
|
+
if (parts[i] !== "") lineBuffer.current.current.push({
|
|
382
|
+
stream: entry.stream,
|
|
383
|
+
text: parts[i],
|
|
384
|
+
ts: entry.ts
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (parts.length > 1 && parts[parts.length - 1] === "") flushLine();
|
|
388
|
+
}, [flushLine]);
|
|
389
|
+
(0, react.useEffect)(() => {
|
|
390
|
+
const url = `/api/dsh-better-sidebar-starter/logs?instanceId=${encodeURIComponent(instance.id)}`;
|
|
391
|
+
const ws = new WebSocket(url);
|
|
392
|
+
wsRef.current = ws;
|
|
393
|
+
ws.onmessage = (event) => {
|
|
394
|
+
try {
|
|
395
|
+
const msg = JSON.parse(event.data);
|
|
396
|
+
if (msg.type === "history") {
|
|
397
|
+
setLogLines([]);
|
|
398
|
+
lineBuffer.current.current = [];
|
|
399
|
+
for (const entry of msg.entries) addEntry(entry);
|
|
400
|
+
flushLine();
|
|
401
|
+
} else if (msg.type === "log") addEntry(msg.entry);
|
|
402
|
+
else if (msg.type === "status") {
|
|
403
|
+
if (msg.status !== "running") {
|
|
404
|
+
flushLine();
|
|
405
|
+
setExitInfo({
|
|
406
|
+
status: msg.status,
|
|
407
|
+
exitCode: msg.exitCode
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
};
|
|
413
|
+
return () => {
|
|
414
|
+
ws.close();
|
|
415
|
+
wsRef.current = null;
|
|
416
|
+
};
|
|
417
|
+
}, [
|
|
418
|
+
instance.id,
|
|
419
|
+
addEntry,
|
|
420
|
+
flushLine
|
|
421
|
+
]);
|
|
422
|
+
(0, react.useEffect)(() => {
|
|
423
|
+
if (autoScroll && contentRef.current !== null) contentRef.current.scrollTop = contentRef.current.scrollHeight;
|
|
424
|
+
}, [logLines, autoScroll]);
|
|
425
|
+
const handleClear = () => {
|
|
426
|
+
setLogLines([]);
|
|
427
|
+
lineBuffer.current.current = [];
|
|
428
|
+
};
|
|
429
|
+
return (0, react.createElement)("div", { style: {
|
|
430
|
+
display: "flex",
|
|
431
|
+
flexDirection: "column",
|
|
432
|
+
height: "100%",
|
|
433
|
+
minHeight: 0
|
|
434
|
+
} }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, instance.configName, (0, react.createElement)("span", { style: {
|
|
435
|
+
opacity: .5,
|
|
436
|
+
marginLeft: "4px"
|
|
437
|
+
} }, `#${instance.id.slice(0, 8)}`), (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), instance.status === "running" ? (0, react.createElement)("button", {
|
|
438
|
+
className: "sts-icon-btn stop",
|
|
439
|
+
title: "停止",
|
|
440
|
+
onClick: onStop
|
|
441
|
+
}, StopIcon({ size: 14 }), " 停止") : null, (0, react.createElement)("button", {
|
|
442
|
+
className: "sts-icon-btn",
|
|
443
|
+
title: "重启",
|
|
444
|
+
onClick: onRestart
|
|
445
|
+
}, RestartIcon({ size: 14 }), " 重启"), (0, react.createElement)("button", {
|
|
446
|
+
className: "sts-icon-btn",
|
|
447
|
+
title: "清空",
|
|
448
|
+
onClick: handleClear
|
|
449
|
+
}, TrashIcon({ size: 14 }), " 清空"), (0, react.createElement)("label", {
|
|
450
|
+
className: "sts-log-toolbar-label",
|
|
451
|
+
onClick: () => setAutoScroll((v) => !v)
|
|
452
|
+
}, autoScroll ? "☑" : "☐", " 自动滚动")), (0, react.createElement)("div", {
|
|
453
|
+
ref: contentRef,
|
|
454
|
+
className: "sts-log-content"
|
|
455
|
+
}, ...logLines.map((line) => {
|
|
456
|
+
const keyStr = `line-${line.key}`;
|
|
457
|
+
return (0, react.createElement)("div", {
|
|
458
|
+
key: keyStr,
|
|
459
|
+
className: "sts-log-line"
|
|
460
|
+
}, ...line.entries.map((entry, i) => {
|
|
461
|
+
const segKey = `${keyStr}-e${i}`;
|
|
462
|
+
return (0, react.createElement)("span", {
|
|
463
|
+
key: segKey,
|
|
464
|
+
className: entry.stream === "stderr" ? "sts-log-stderr" : ""
|
|
465
|
+
}, renderAnsiLine(entry.text, segKey));
|
|
466
|
+
}));
|
|
467
|
+
}), exitInfo !== null ? (0, react.createElement)("div", {
|
|
468
|
+
key: "exit-info",
|
|
469
|
+
style: {
|
|
470
|
+
color: exitInfo.status === "exited" && exitInfo.exitCode === 0 ? "#4e9a06" : "#cc0000",
|
|
471
|
+
fontWeight: "bold",
|
|
472
|
+
marginTop: "4px"
|
|
473
|
+
}
|
|
474
|
+
}, `\n进程${exitInfo.status === "exited" ? "退出" : exitInfo.status === "killed" ? "被终止" : "出错"}`, exitInfo.exitCode !== null ? `,退出码: ${exitInfo.exitCode}` : "") : null));
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/client/ConfigDetail.tsx
|
|
478
|
+
/**
|
|
479
|
+
* Config detail card: shown in the right pane when an idle config is selected.
|
|
480
|
+
* Displays the config's full info and offers start / edit / delete buttons.
|
|
481
|
+
* @module dsh-better-sidebar-starter/client/ConfigDetail
|
|
482
|
+
*/
|
|
483
|
+
const TYPE_LABELS$1 = {
|
|
484
|
+
npm: "npm",
|
|
485
|
+
springboot: "Spring Boot",
|
|
486
|
+
python: "Python",
|
|
487
|
+
custom: "Custom"
|
|
488
|
+
};
|
|
489
|
+
/** The detail card component. */
|
|
490
|
+
function ConfigDetail({ config, onStart, onEdit, onDelete }) {
|
|
491
|
+
const envEntries = Object.entries(config.env);
|
|
492
|
+
const dateFmt = (ts) => {
|
|
493
|
+
if (ts === null) return "—";
|
|
494
|
+
return new Date(ts).toLocaleString("zh-CN", { hour12: false });
|
|
495
|
+
};
|
|
496
|
+
return (0, react.createElement)("div", { className: "sts-detail" }, (0, react.createElement)("div", { className: "sts-detail-card" }, (0, react.createElement)("div", { className: "sts-detail-title" }, typeIcon(config.type, 20), config.name), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "类型"), (0, react.createElement)("div", { className: "sts-detail-value" }, TYPE_LABELS$1[config.type] ?? config.type)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "命令"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.command)), (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "工作目录"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.cwd)), config.type === "springboot" && config.jvmArgs ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "JVM参数"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.jvmArgs)) : null, config.args ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "程序实参"), (0, react.createElement)("div", { className: "sts-detail-value" }, config.args)) : null, envEntries.length > 0 ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "环境变量"), (0, react.createElement)("div", { style: { flex: 1 } }, ...envEntries.map(([key, value]) => (0, react.createElement)("div", {
|
|
497
|
+
key,
|
|
498
|
+
className: "sts-detail-value"
|
|
499
|
+
}, `${key} = ${value}`)))) : null, (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "创建时间"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.createdAt))), config.lastRunAt !== null ? (0, react.createElement)("div", { className: "sts-detail-row" }, (0, react.createElement)("div", { className: "sts-detail-label" }, "最近运行"), (0, react.createElement)("div", { className: "sts-detail-value" }, dateFmt(config.lastRunAt))) : null, (0, react.createElement)("div", { className: "sts-detail-actions" }, (0, react.createElement)("button", {
|
|
500
|
+
className: "sts-detail-btn danger",
|
|
501
|
+
onClick: onDelete
|
|
502
|
+
}, (0, react.createElement)(TrashIcon, { size: 14 }), " 删除"), (0, react.createElement)("button", {
|
|
503
|
+
className: "sts-detail-btn",
|
|
504
|
+
onClick: onEdit
|
|
505
|
+
}, (0, react.createElement)(GearIcon, { size: 14 }), " 编辑"), (0, react.createElement)("button", {
|
|
506
|
+
className: "sts-detail-btn primary",
|
|
507
|
+
onClick: onStart,
|
|
508
|
+
style: {
|
|
509
|
+
background: "#1a1a1e",
|
|
510
|
+
border: "1px solid #3a3a3e",
|
|
511
|
+
color: "#fff",
|
|
512
|
+
borderRadius: "6px",
|
|
513
|
+
padding: "6px 14px",
|
|
514
|
+
cursor: "pointer",
|
|
515
|
+
fontSize: "12px"
|
|
516
|
+
}
|
|
517
|
+
}, (0, react.createElement)(PlayIcon, { size: 14 }), " 启动"))));
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/client/DirChooser.tsx
|
|
521
|
+
/**
|
|
522
|
+
* Shared directory chooser: a filesystem folder tree (backed by the sidebar
|
|
523
|
+
* fs.tree API, which can list any absolute path) plus a browse button that
|
|
524
|
+
* opens a modal popup. Browsing starts at the user's home directory and can
|
|
525
|
+
* navigate up to any drive/root, so runtime tool paths (JDK/Maven/...) that
|
|
526
|
+
* live outside the workspace can be selected.
|
|
527
|
+
* @module dsh-better-sidebar-starter/client/DirChooser
|
|
528
|
+
*/
|
|
529
|
+
/** Call the sidebar fs.tree API directly via fetch. Returns directories only. */
|
|
530
|
+
async function fsTree$1(scope, path) {
|
|
531
|
+
const payload = {
|
|
532
|
+
sessionId: scope.sessionId,
|
|
533
|
+
path
|
|
534
|
+
};
|
|
535
|
+
if (scope.cwd !== void 0 && scope.cwd !== "") payload.cwd = scope.cwd;
|
|
536
|
+
const resp = await fetch("/sidebar/api/fs.tree", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: { "content-type": "application/json" },
|
|
539
|
+
body: JSON.stringify(payload)
|
|
540
|
+
});
|
|
541
|
+
if (!resp.ok) throw new Error(`fs.tree ${path}: ${resp.status}`);
|
|
542
|
+
const parsed = await resp.json();
|
|
543
|
+
if (!parsed.ok || parsed.value === void 0) throw new Error(`fs.tree ${path}: ${parsed.error?.message ?? "unknown error"}`);
|
|
544
|
+
return parsed.value.entries.filter((e) => e.isDir && !e.hidden);
|
|
545
|
+
}
|
|
546
|
+
/** Fetch the user's home directory from the host. Falls back to cwd. */
|
|
547
|
+
async function fetchHomeDefault(scope) {
|
|
548
|
+
try {
|
|
549
|
+
const r = await fetch("/api/dsh-better-sidebar-starter/detect-env", { method: "GET" });
|
|
550
|
+
if (r.ok) {
|
|
551
|
+
const data = await r.json();
|
|
552
|
+
if (data.home && data.home !== "") return data.home;
|
|
553
|
+
}
|
|
554
|
+
} catch {}
|
|
555
|
+
return scope.cwd ?? ".";
|
|
556
|
+
}
|
|
557
|
+
/** Normalize a path, returning its parent or null if at the root. */
|
|
558
|
+
function parentOf(p) {
|
|
559
|
+
const trimmed = p.replace(/[\\/]+$/, "");
|
|
560
|
+
const lastSep = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
561
|
+
if (lastSep <= 0) return null;
|
|
562
|
+
return trimmed.substring(0, lastSep);
|
|
563
|
+
}
|
|
564
|
+
/** The folder tree panel: shows the current dir's subdirectories with up-nav. */
|
|
565
|
+
function DirTree(props) {
|
|
566
|
+
const { scope, selectedPath, onPick } = props;
|
|
567
|
+
const [cur, setCur] = (0, react.useState)(".");
|
|
568
|
+
const [entries, setEntries] = (0, react.useState)(null);
|
|
569
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
570
|
+
const [error, setError] = (0, react.useState)(null);
|
|
571
|
+
const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
572
|
+
const scopeRef = (0, react.useRef)(scope);
|
|
573
|
+
const initDoneRef = (0, react.useRef)(false);
|
|
574
|
+
const initialValueRef = (0, react.useRef)(selectedPath);
|
|
575
|
+
(0, react.useEffect)(() => {
|
|
576
|
+
if (initDoneRef.current) return;
|
|
577
|
+
initDoneRef.current = true;
|
|
578
|
+
let cancelled = false;
|
|
579
|
+
const s = scopeRef.current;
|
|
580
|
+
const startPath = initialValueRef.current?.trim() && initialValueRef.current !== "." && initialValueRef.current !== "./" ? initialValueRef.current.trim() : null;
|
|
581
|
+
if (startPath !== null) {
|
|
582
|
+
setCur(startPath);
|
|
583
|
+
loadDir(s, startPath);
|
|
584
|
+
} else fetchHomeDefault(s).then((home) => {
|
|
585
|
+
if (cancelled) return;
|
|
586
|
+
setCur(home);
|
|
587
|
+
loadDir(s, home);
|
|
588
|
+
});
|
|
589
|
+
return () => {
|
|
590
|
+
cancelled = true;
|
|
591
|
+
};
|
|
592
|
+
}, []);
|
|
593
|
+
const loadDir = (s, path) => {
|
|
594
|
+
setLoading(true);
|
|
595
|
+
setError(null);
|
|
596
|
+
fsTree$1(s, path).then((dirs) => {
|
|
597
|
+
setEntries(dirs);
|
|
598
|
+
setLoading(false);
|
|
599
|
+
}).catch((err) => {
|
|
600
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
601
|
+
setLoading(false);
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
const goUp = () => {
|
|
605
|
+
const parent = parentOf(cur);
|
|
606
|
+
if (parent !== null) {
|
|
607
|
+
setCur(parent);
|
|
608
|
+
loadDir(scopeRef.current, parent);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const navigateInto = (path) => {
|
|
612
|
+
setCur(path);
|
|
613
|
+
loadDir(scopeRef.current, path);
|
|
614
|
+
};
|
|
615
|
+
const toggleExpand = (path) => {
|
|
616
|
+
setExpanded((prev) => {
|
|
617
|
+
const next = new Set(prev);
|
|
618
|
+
if (next.has(path)) next.delete(path);
|
|
619
|
+
else next.add(path);
|
|
620
|
+
return next;
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
const isSelected = (p) => selectedPath !== "" && selectedPath !== "." && selectedPath === p;
|
|
624
|
+
return (0, react.createElement)("div", { className: "sts-dirtree" }, (0, react.createElement)("div", { className: "sts-dirtree-nav" }, (0, react.createElement)("button", {
|
|
625
|
+
className: "sts-dirtree-up",
|
|
626
|
+
onClick: goUp,
|
|
627
|
+
disabled: parentOf(cur) === null,
|
|
628
|
+
title: "上级目录"
|
|
629
|
+
}, "↑"), (0, react.createElement)("button", {
|
|
630
|
+
className: "sts-dirtree-home",
|
|
631
|
+
onClick: () => loadDir(scopeRef.current, cur),
|
|
632
|
+
title: "刷新"
|
|
633
|
+
}, "↻"), (0, react.createElement)("span", {
|
|
634
|
+
className: "sts-dirtree-cur",
|
|
635
|
+
title: cur
|
|
636
|
+
}, cur || "(.)")), (0, react.createElement)("div", {
|
|
637
|
+
className: `sts-tree-row${isSelected(cur) ? " selected" : ""}`,
|
|
638
|
+
style: { paddingLeft: "8px" },
|
|
639
|
+
onClick: () => onPick(cur)
|
|
640
|
+
}, (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, cur || "(.)"), (0, react.createElement)("span", { className: "sts-tree-select-hint" }, "● 选择此目录")), loading ? (0, react.createElement)("div", {
|
|
641
|
+
className: "sts-tree-loading",
|
|
642
|
+
style: { paddingLeft: "16px" }
|
|
643
|
+
}, "加载中...") : error !== null ? (0, react.createElement)("div", {
|
|
644
|
+
className: "sts-tree-error",
|
|
645
|
+
style: { paddingLeft: "16px" }
|
|
646
|
+
}, error) : entries !== null && entries.length > 0 ? (0, react.createElement)("div", { className: "sts-dirtree-sub" }, ...entries.map((entry) => (0, react.createElement)(DirRow, {
|
|
647
|
+
key: entry.path,
|
|
648
|
+
scope,
|
|
649
|
+
entry,
|
|
650
|
+
depth: 1,
|
|
651
|
+
selectedPath,
|
|
652
|
+
expanded,
|
|
653
|
+
onToggleExpand: toggleExpand,
|
|
654
|
+
onPick,
|
|
655
|
+
onNavigate: navigateInto
|
|
656
|
+
}))) : (0, react.createElement)("div", {
|
|
657
|
+
className: "sts-tree-empty",
|
|
658
|
+
style: { paddingLeft: "16px" }
|
|
659
|
+
}, "无子目录"));
|
|
660
|
+
}
|
|
661
|
+
/** One directory row with recursive expansion + click into. */
|
|
662
|
+
function DirRow(props) {
|
|
663
|
+
const { scope, entry, depth, selectedPath, expanded, onToggleExpand, onPick, onNavigate } = props;
|
|
664
|
+
const [children, setChildren] = (0, react.useState)(null);
|
|
665
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
666
|
+
const isExpanded = expanded.has(entry.path);
|
|
667
|
+
const isSelected = selectedPath === entry.path;
|
|
668
|
+
const handleClick = () => {
|
|
669
|
+
onPick(entry.path);
|
|
670
|
+
if (isExpanded) {
|
|
671
|
+
onToggleExpand(entry.path);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
onToggleExpand(entry.path);
|
|
675
|
+
if (children === null) {
|
|
676
|
+
setLoading(true);
|
|
677
|
+
fsTree$1(scope, entry.path).then((dirs) => {
|
|
678
|
+
setChildren(dirs);
|
|
679
|
+
setLoading(false);
|
|
680
|
+
}).catch(() => {
|
|
681
|
+
setChildren([]);
|
|
682
|
+
setLoading(false);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", {
|
|
687
|
+
className: `sts-tree-row${isSelected ? " selected" : ""}`,
|
|
688
|
+
style: { paddingLeft: `${depth * 16 + 8}px` },
|
|
689
|
+
onClick: handleClick
|
|
690
|
+
}, (0, react.createElement)("span", { className: `sts-tree-chevron${isExpanded ? " expanded" : ""}` }, (0, react.createElement)(isExpanded ? ChevronDownIcon : ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, entry.name)), isExpanded ? loading ? (0, react.createElement)("div", {
|
|
691
|
+
className: "sts-tree-loading",
|
|
692
|
+
style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
|
|
693
|
+
}, "加载中...") : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(DirRow, {
|
|
694
|
+
key: child.path,
|
|
695
|
+
scope,
|
|
696
|
+
entry: child,
|
|
697
|
+
depth: depth + 1,
|
|
698
|
+
selectedPath,
|
|
699
|
+
expanded,
|
|
700
|
+
onToggleExpand,
|
|
701
|
+
onPick,
|
|
702
|
+
onNavigate: props.onNavigate
|
|
703
|
+
}))) : null : null);
|
|
704
|
+
}
|
|
705
|
+
/** A read-only path input + browse button that opens a modal folder tree. */
|
|
706
|
+
function DirChooser(props) {
|
|
707
|
+
const { scope, value, onChange, compact, placeholder } = props;
|
|
708
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
709
|
+
return (0, react.createElement)("div", { className: "sts-settings-path-row" }, (0, react.createElement)("span", { className: "sts-settings-path-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("input", {
|
|
710
|
+
className: compact ? "sts-modal-input mono" : "sts-settings-input",
|
|
711
|
+
value,
|
|
712
|
+
readOnly: true,
|
|
713
|
+
placeholder: placeholder ?? "",
|
|
714
|
+
title: value,
|
|
715
|
+
onClick: () => setOpen(true)
|
|
716
|
+
}), (0, react.createElement)("button", {
|
|
717
|
+
className: "sts-settings-browse",
|
|
718
|
+
onClick: () => setOpen(true),
|
|
719
|
+
title: "选择文件夹"
|
|
720
|
+
}, "浏览"), open ? (0, react.createElement)(DirChooserModal, {
|
|
721
|
+
scope,
|
|
722
|
+
value,
|
|
723
|
+
onConfirm: (p) => {
|
|
724
|
+
onChange(p);
|
|
725
|
+
setOpen(false);
|
|
726
|
+
},
|
|
727
|
+
onCancel: () => setOpen(false)
|
|
728
|
+
}) : null);
|
|
729
|
+
}
|
|
730
|
+
/** Modal popup hosting the folder tree with confirm/cancel. */
|
|
731
|
+
function DirChooserModal(props) {
|
|
732
|
+
const { scope, value, onConfirm, onCancel } = props;
|
|
733
|
+
const [sel, setSel] = (0, react.useState)(value);
|
|
734
|
+
return (0, react.createElement)("div", {
|
|
735
|
+
className: "sts-modal-overlay",
|
|
736
|
+
onClick: (e) => {
|
|
737
|
+
if (e.target === e.currentTarget) onCancel();
|
|
738
|
+
}
|
|
739
|
+
}, (0, react.createElement)("div", { className: "sts-dir-modal" }, (0, react.createElement)("div", { className: "sts-modal-header" }, "选择文件夹", (0, react.createElement)("button", {
|
|
740
|
+
className: "sts-icon-btn",
|
|
741
|
+
onClick: onCancel
|
|
742
|
+
}, (0, react.createElement)(CloseIcon, { size: 16 }))), (0, react.createElement)("div", { className: "sts-dir-modal-tree" }, (0, react.createElement)(DirTree, {
|
|
743
|
+
scope,
|
|
744
|
+
selectedPath: sel,
|
|
745
|
+
onPick: setSel
|
|
746
|
+
})), (0, react.createElement)("div", { className: "sts-modal-footer" }, (0, react.createElement)("button", {
|
|
747
|
+
className: "sts-modal-btn",
|
|
748
|
+
onClick: onCancel
|
|
749
|
+
}, "取消"), (0, react.createElement)("button", {
|
|
750
|
+
className: "sts-modal-btn primary",
|
|
751
|
+
onClick: () => onConfirm(sel),
|
|
752
|
+
style: {
|
|
753
|
+
background: "#1a1a1e",
|
|
754
|
+
border: "1px solid #3a3a3e",
|
|
755
|
+
color: "#fff",
|
|
756
|
+
borderRadius: "6px",
|
|
757
|
+
padding: "8px 18px",
|
|
758
|
+
cursor: "pointer",
|
|
759
|
+
fontSize: "13px"
|
|
760
|
+
}
|
|
761
|
+
}, (0, react.createElement)(CheckIcon, { size: 14 }), " 确定"))));
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/client/PathPicker.tsx
|
|
765
|
+
/**
|
|
766
|
+
* Path picker: a read-only input + browse button that opens a folder tree
|
|
767
|
+
* modal (DirChooser). Shared by the settings panel and the config editor.
|
|
768
|
+
* @module dsh-better-sidebar-starter/client/PathPicker
|
|
769
|
+
*/
|
|
770
|
+
/** A folder-browse input with a hosted modal tree chooser. */
|
|
771
|
+
function PathPicker(props) {
|
|
772
|
+
const { scope, value, onChange, compact, desc } = props;
|
|
773
|
+
return (0, react.createElement)(DirChooser, {
|
|
774
|
+
scope,
|
|
775
|
+
value,
|
|
776
|
+
onChange,
|
|
777
|
+
compact,
|
|
778
|
+
placeholder: desc
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/client/ConfigEditorModal.tsx
|
|
783
|
+
/**
|
|
784
|
+
* Config editor modal dialog: create or edit a run configuration.
|
|
785
|
+
* Lets the user set name, type, command, cwd (tree picker), jvmArgs, args,
|
|
786
|
+
* and environment variables.
|
|
787
|
+
* @module dsh-better-sidebar-starter/client/ConfigEditorModal
|
|
788
|
+
*/
|
|
789
|
+
/** Call the sidebar fs.tree API directly via fetch. Returns directories only.
|
|
790
|
+
* If path is omitted, the server uses the session cwd (root listing).
|
|
791
|
+
* The sidebar API wraps responses in { ok: true, value: ... } envelope. */
|
|
792
|
+
async function fsTree(scope, path) {
|
|
793
|
+
const payload = { sessionId: scope.sessionId };
|
|
794
|
+
if (scope.cwd !== void 0 && scope.cwd !== "") payload.cwd = scope.cwd;
|
|
795
|
+
if (path !== void 0) payload.path = path;
|
|
796
|
+
const resp = await fetch("/sidebar/api/fs.tree", {
|
|
797
|
+
method: "POST",
|
|
798
|
+
headers: { "content-type": "application/json" },
|
|
799
|
+
body: JSON.stringify(payload)
|
|
800
|
+
});
|
|
801
|
+
if (!resp.ok) throw new Error(`fs.tree ${path ?? "(root)"}: ${resp.status}`);
|
|
802
|
+
const parsed = await resp.json();
|
|
803
|
+
if (!parsed.ok || parsed.value === void 0) throw new Error(`fs.tree ${path ?? "(root)"}: ${parsed.error?.message ?? "unknown error"}`);
|
|
804
|
+
return parsed.value.entries.filter((e) => e.isDir && !e.hidden);
|
|
805
|
+
}
|
|
806
|
+
/** Recursive tree node: loads its own children on expand. */
|
|
807
|
+
function TreeNode(props) {
|
|
808
|
+
const { scope, entry, depth, selectedPath, onSelect } = props;
|
|
809
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
810
|
+
const [children, setChildren] = (0, react.useState)(null);
|
|
811
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
812
|
+
const handleToggle = () => {
|
|
813
|
+
if (!expanded && children === null) {
|
|
814
|
+
setLoading(true);
|
|
815
|
+
fsTree(scope, entry.path).then((dirs) => {
|
|
816
|
+
setChildren(dirs);
|
|
817
|
+
setLoading(false);
|
|
818
|
+
}).catch(() => {
|
|
819
|
+
setChildren([]);
|
|
820
|
+
setLoading(false);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
setExpanded(!expanded);
|
|
824
|
+
};
|
|
825
|
+
const isSelected = selectedPath === entry.path;
|
|
826
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", {
|
|
827
|
+
className: `sts-tree-row${isSelected ? " selected" : ""}`,
|
|
828
|
+
style: { paddingLeft: `${depth * 16 + 8}px` },
|
|
829
|
+
onClick: () => {
|
|
830
|
+
onSelect(entry.path);
|
|
831
|
+
handleToggle();
|
|
832
|
+
}
|
|
833
|
+
}, (0, react.createElement)("span", { className: `sts-tree-chevron${expanded ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, entry.name)), expanded ? loading ? (0, react.createElement)("div", {
|
|
834
|
+
className: "sts-tree-loading",
|
|
835
|
+
style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
|
|
836
|
+
}, "加载中...") : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(TreeNode, {
|
|
837
|
+
key: child.path,
|
|
838
|
+
scope,
|
|
839
|
+
entry: child,
|
|
840
|
+
depth: depth + 1,
|
|
841
|
+
selectedPath,
|
|
842
|
+
onSelect
|
|
843
|
+
}))) : (0, react.createElement)("div", {
|
|
844
|
+
className: "sts-tree-empty",
|
|
845
|
+
style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }
|
|
846
|
+
}, "无子目录") : null);
|
|
847
|
+
}
|
|
848
|
+
/** Directory tree picker: shows a collapsible tree rooted at session cwd. */
|
|
849
|
+
function DirTreePicker(props) {
|
|
850
|
+
const { scope, value, onSelect } = props;
|
|
851
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
852
|
+
const [children, setChildren] = (0, react.useState)(null);
|
|
853
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
854
|
+
const [error, setError] = (0, react.useState)(null);
|
|
855
|
+
const handleToggle = () => {
|
|
856
|
+
if (!expanded && children === null) {
|
|
857
|
+
setLoading(true);
|
|
858
|
+
setError(null);
|
|
859
|
+
fsTree(scope).then((dirs) => {
|
|
860
|
+
setChildren(dirs);
|
|
861
|
+
setLoading(false);
|
|
862
|
+
}).catch((err) => {
|
|
863
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
864
|
+
setLoading(false);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
setExpanded(!expanded);
|
|
868
|
+
};
|
|
869
|
+
return (0, react.createElement)("div", { className: "sts-tree-picker" }, (0, react.createElement)("div", {
|
|
870
|
+
className: `sts-tree-row${value === "." || value === "" || value === "./" ? " selected" : ""}`,
|
|
871
|
+
style: { paddingLeft: "8px" },
|
|
872
|
+
onClick: () => {
|
|
873
|
+
onSelect(".");
|
|
874
|
+
handleToggle();
|
|
875
|
+
}
|
|
876
|
+
}, (0, react.createElement)("span", { className: `sts-tree-chevron${expanded ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", { className: "sts-tree-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-tree-label" }, "(工作空间根目录)")), expanded ? loading ? (0, react.createElement)("div", {
|
|
877
|
+
className: "sts-tree-loading",
|
|
878
|
+
style: { paddingLeft: "24px" }
|
|
879
|
+
}, "加载中...") : error !== null ? (0, react.createElement)("div", {
|
|
880
|
+
className: "sts-tree-error",
|
|
881
|
+
style: { paddingLeft: "24px" }
|
|
882
|
+
}, error) : children !== null && children.length > 0 ? (0, react.createElement)("div", null, ...children.map((child) => (0, react.createElement)(TreeNode, {
|
|
883
|
+
key: child.path,
|
|
884
|
+
scope,
|
|
885
|
+
entry: child,
|
|
886
|
+
depth: 1,
|
|
887
|
+
selectedPath: value,
|
|
888
|
+
onSelect
|
|
889
|
+
}))) : (0, react.createElement)("div", {
|
|
890
|
+
className: "sts-tree-empty",
|
|
891
|
+
style: { paddingLeft: "24px" }
|
|
892
|
+
}, "无子目录") : null);
|
|
893
|
+
}
|
|
894
|
+
/** Type \u2192 default command mapping. */
|
|
895
|
+
const TYPE_DEFAULTS = {
|
|
896
|
+
npm: "npm run dev",
|
|
897
|
+
springboot: "mvn spring-boot:run",
|
|
898
|
+
python: "python main.py",
|
|
899
|
+
custom: ""
|
|
900
|
+
};
|
|
901
|
+
const TYPE_LABELS = {
|
|
902
|
+
npm: "npm",
|
|
903
|
+
springboot: "Spring Boot",
|
|
904
|
+
python: "Python",
|
|
905
|
+
custom: "Custom"
|
|
906
|
+
};
|
|
907
|
+
/** The modal dialog component. */
|
|
908
|
+
function ConfigEditorModal({ config, scope, onSave, onCancel }) {
|
|
909
|
+
const [name, setName] = (0, react.useState)(config?.name ?? "");
|
|
910
|
+
const [type, setType] = (0, react.useState)(config?.type ?? "custom");
|
|
911
|
+
const [command, setCommand] = (0, react.useState)(config?.command ?? "");
|
|
912
|
+
const [cwd, setCwd] = (0, react.useState)(config?.cwd ?? ".");
|
|
913
|
+
const [jvmArgs, setJvmArgs] = (0, react.useState)(config?.jvmArgs ?? "");
|
|
914
|
+
const [args, setArgs] = (0, react.useState)(config?.args ?? "");
|
|
915
|
+
const [envRows, setEnvRows] = (0, react.useState)(config ? Object.entries(config.env).map(([key, value]) => ({
|
|
916
|
+
key,
|
|
917
|
+
value
|
|
918
|
+
})) : []);
|
|
919
|
+
const [showTree, setShowTree] = (0, react.useState)(false);
|
|
920
|
+
const [showRuntime, setShowRuntime] = (0, react.useState)(false);
|
|
921
|
+
const [rtJava, setRtJava] = (0, react.useState)(config?.runtime?.java ?? "");
|
|
922
|
+
const [rtNode, setRtNode] = (0, react.useState)(config?.runtime?.node ?? "");
|
|
923
|
+
const [rtPython, setRtPython] = (0, react.useState)(config?.runtime?.python ?? "");
|
|
924
|
+
const [rtMvn, setRtMvn] = (0, react.useState)(config?.runtime?.mvn ?? "");
|
|
925
|
+
(0, react.useEffect)(() => {
|
|
926
|
+
if (command === "" && TYPE_DEFAULTS[type] !== "") setCommand(TYPE_DEFAULTS[type]);
|
|
927
|
+
}, [type]);
|
|
928
|
+
const handleSave = () => {
|
|
929
|
+
const env = {};
|
|
930
|
+
for (const row of envRows) if (row.key.trim() !== "") env[row.key.trim()] = row.value;
|
|
931
|
+
const runtime = {};
|
|
932
|
+
if (rtJava.trim() !== "") runtime.java = rtJava.trim();
|
|
933
|
+
if (rtNode.trim() !== "") runtime.node = rtNode.trim();
|
|
934
|
+
if (rtPython.trim() !== "") runtime.python = rtPython.trim();
|
|
935
|
+
if (rtMvn.trim() !== "") runtime.mvn = rtMvn.trim();
|
|
936
|
+
onSave({
|
|
937
|
+
id: config?.id,
|
|
938
|
+
name: name.trim(),
|
|
939
|
+
type,
|
|
940
|
+
command: command.trim(),
|
|
941
|
+
cwd: cwd.trim() === "" ? "." : cwd.trim(),
|
|
942
|
+
env,
|
|
943
|
+
jvmArgs: jvmArgs.trim(),
|
|
944
|
+
args: args.trim(),
|
|
945
|
+
runtime
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
const updateEnvRow = (idx, field, val) => {
|
|
949
|
+
setEnvRows((prev) => prev.map((row, i) => i === idx ? {
|
|
950
|
+
...row,
|
|
951
|
+
[field]: val
|
|
952
|
+
} : row));
|
|
953
|
+
};
|
|
954
|
+
const addEnvRow = () => setEnvRows((prev) => [...prev, {
|
|
955
|
+
key: "",
|
|
956
|
+
value: ""
|
|
957
|
+
}]);
|
|
958
|
+
const removeEnvRow = (idx) => setEnvRows((prev) => prev.filter((_, i) => i !== idx));
|
|
959
|
+
const [showEnvPaste, setShowEnvPaste] = (0, react.useState)(false);
|
|
960
|
+
const [envPasteText, setEnvPasteText] = (0, react.useState)("");
|
|
961
|
+
/** Parse "K=V;K=V,..." (or newline/comma separated) into env rows, merging
|
|
962
|
+
* into the existing rows (later values override earlier for duplicate keys). */
|
|
963
|
+
const applyEnvPaste = () => {
|
|
964
|
+
const existing = new Map(envRows.filter((r) => r.key.trim() !== "").map((r) => [r.key.trim(), r.value]));
|
|
965
|
+
const text = envPasteText.trim();
|
|
966
|
+
if (text === "") return;
|
|
967
|
+
const pairs = text.split(/[;,\n]+/);
|
|
968
|
+
for (const pair of pairs) {
|
|
969
|
+
const p = pair.trim();
|
|
970
|
+
if (p === "") continue;
|
|
971
|
+
const eq = p.indexOf("=");
|
|
972
|
+
if (eq <= 0) continue;
|
|
973
|
+
const key = p.substring(0, eq).trim();
|
|
974
|
+
const value = p.substring(eq + 1).trim();
|
|
975
|
+
if (key !== "") existing.set(key, value);
|
|
976
|
+
}
|
|
977
|
+
setEnvRows(Array.from(existing, ([key, value]) => ({
|
|
978
|
+
key,
|
|
979
|
+
value
|
|
980
|
+
})));
|
|
981
|
+
setEnvPasteText("");
|
|
982
|
+
setShowEnvPaste(false);
|
|
983
|
+
};
|
|
984
|
+
const isSpringboot = type === "springboot";
|
|
985
|
+
return (0, react.createElement)("div", {
|
|
986
|
+
className: "sts-modal-overlay",
|
|
987
|
+
onClick: (e) => {
|
|
988
|
+
if (e.target === e.currentTarget) onCancel();
|
|
989
|
+
}
|
|
990
|
+
}, (0, react.createElement)("div", { className: "sts-modal" }, (0, react.createElement)("div", { className: "sts-modal-header" }, config?.id ? "编辑运行配置" : "新建运行配置", (0, react.createElement)("button", {
|
|
991
|
+
className: "sts-icon-btn",
|
|
992
|
+
onClick: onCancel
|
|
993
|
+
}, (0, react.createElement)(CloseIcon, { size: 16 }))), (0, react.createElement)("div", { className: "sts-modal-body" }, (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "名称"), (0, react.createElement)("input", {
|
|
994
|
+
className: "sts-modal-input",
|
|
995
|
+
value: name,
|
|
996
|
+
onChange: (e) => setName(e.target.value),
|
|
997
|
+
placeholder: "配置名称"
|
|
998
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "类型"), (0, react.createElement)("select", {
|
|
999
|
+
className: "sts-modal-select",
|
|
1000
|
+
value: type,
|
|
1001
|
+
onChange: (e) => setType(e.target.value)
|
|
1002
|
+
}, ...Object.entries(TYPE_LABELS).map(([val, label]) => (0, react.createElement)("option", {
|
|
1003
|
+
key: val,
|
|
1004
|
+
value: val
|
|
1005
|
+
}, label)))), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "命令"), (0, react.createElement)("input", {
|
|
1006
|
+
className: "sts-modal-input mono",
|
|
1007
|
+
value: command,
|
|
1008
|
+
onChange: (e) => setCommand(e.target.value),
|
|
1009
|
+
placeholder: "要执行的命令"
|
|
1010
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "工作目录"), (0, react.createElement)("div", {
|
|
1011
|
+
className: "sts-cwd-display",
|
|
1012
|
+
onClick: () => setShowTree(!showTree)
|
|
1013
|
+
}, (0, react.createElement)("span", { className: "sts-cwd-icon" }, (0, react.createElement)(FolderIcon, { size: 14 })), (0, react.createElement)("span", { className: "sts-cwd-value" }, cwd)), showTree ? (0, react.createElement)("div", { className: "sts-tree-picker-container" }, (0, react.createElement)(DirTreePicker, {
|
|
1014
|
+
scope,
|
|
1015
|
+
value: cwd,
|
|
1016
|
+
onSelect: (p) => setCwd(p)
|
|
1017
|
+
})) : null), isSpringboot ? (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "虚拟机参数 (JVM Args)"), (0, react.createElement)("input", {
|
|
1018
|
+
className: "sts-modal-input mono",
|
|
1019
|
+
value: jvmArgs,
|
|
1020
|
+
onChange: (e) => setJvmArgs(e.target.value),
|
|
1021
|
+
placeholder: "例: -Xmx512m -Dspring.profiles.active=dev"
|
|
1022
|
+
})) : null, (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "程序实参"), (0, react.createElement)("input", {
|
|
1023
|
+
className: "sts-modal-input mono",
|
|
1024
|
+
value: args,
|
|
1025
|
+
onChange: (e) => setArgs(e.target.value),
|
|
1026
|
+
placeholder: "例: --server.port=8080 --debug"
|
|
1027
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("div", {
|
|
1028
|
+
className: `sts-collapse-header${showRuntime ? " open" : ""}`,
|
|
1029
|
+
onClick: () => setShowRuntime(!showRuntime)
|
|
1030
|
+
}, (0, react.createElement)("span", { className: `sts-collapse-chevron${showRuntime ? " expanded" : ""}` }, (0, react.createElement)(ChevronRightIcon, { size: 12 })), (0, react.createElement)("span", null, "运行时环境 (高级)"), (0, react.createElement)("span", { className: "sts-collapse-hint" }, "默认用全局配置")), showRuntime ? (0, react.createElement)("div", { className: "sts-collapse-body" }, (0, react.createElement)("div", { className: "sts-modal-label" }, "留空则用全局/系统配置"), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Java 路径"), (0, react.createElement)(PathPicker, {
|
|
1031
|
+
scope,
|
|
1032
|
+
kind: "java",
|
|
1033
|
+
value: rtJava,
|
|
1034
|
+
onChange: setRtJava,
|
|
1035
|
+
compact: true,
|
|
1036
|
+
desc: "选择 JDK 目录"
|
|
1037
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Node.js 路径"), (0, react.createElement)(PathPicker, {
|
|
1038
|
+
scope,
|
|
1039
|
+
kind: "node",
|
|
1040
|
+
value: rtNode,
|
|
1041
|
+
onChange: setRtNode,
|
|
1042
|
+
compact: true,
|
|
1043
|
+
desc: "选择 Node 目录"
|
|
1044
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Python 路径"), (0, react.createElement)(PathPicker, {
|
|
1045
|
+
scope,
|
|
1046
|
+
kind: "python",
|
|
1047
|
+
value: rtPython,
|
|
1048
|
+
onChange: setRtPython,
|
|
1049
|
+
compact: true,
|
|
1050
|
+
desc: "选择 Python 目录"
|
|
1051
|
+
})), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "Maven 路径 (MVN)"), (0, react.createElement)(PathPicker, {
|
|
1052
|
+
scope,
|
|
1053
|
+
kind: "mvn",
|
|
1054
|
+
value: rtMvn,
|
|
1055
|
+
onChange: setRtMvn,
|
|
1056
|
+
compact: true,
|
|
1057
|
+
desc: "选择 Maven 目录"
|
|
1058
|
+
}))) : null), (0, react.createElement)("div", { className: "sts-modal-field" }, (0, react.createElement)("label", { className: "sts-modal-label" }, "环境变量"), ...envRows.map((row, idx) => (0, react.createElement)("div", {
|
|
1059
|
+
key: idx,
|
|
1060
|
+
className: "sts-env-row"
|
|
1061
|
+
}, (0, react.createElement)("input", {
|
|
1062
|
+
className: "sts-modal-input sts-env-key",
|
|
1063
|
+
value: row.key,
|
|
1064
|
+
onChange: (e) => updateEnvRow(idx, "key", e.target.value),
|
|
1065
|
+
placeholder: "变量名 (如 JAVA_HOME)"
|
|
1066
|
+
}), (0, react.createElement)("span", { className: "sts-env-sep" }, "="), (0, react.createElement)("input", {
|
|
1067
|
+
className: "sts-modal-input sts-env-val",
|
|
1068
|
+
value: row.value,
|
|
1069
|
+
onChange: (e) => updateEnvRow(idx, "value", e.target.value),
|
|
1070
|
+
placeholder: "变量值"
|
|
1071
|
+
}), (0, react.createElement)("button", {
|
|
1072
|
+
className: "sts-icon-btn trash",
|
|
1073
|
+
onClick: () => removeEnvRow(idx)
|
|
1074
|
+
}, (0, react.createElement)(TrashIcon, { size: 14 })))), (0, react.createElement)("div", { className: "sts-env-actions" }, (0, react.createElement)("button", {
|
|
1075
|
+
className: "sts-env-add",
|
|
1076
|
+
onClick: addEnvRow
|
|
1077
|
+
}, (0, react.createElement)(PlusIcon, { size: 12 }), " 添加环境变量"), (0, react.createElement)("button", {
|
|
1078
|
+
className: "sts-env-add",
|
|
1079
|
+
onClick: () => setShowEnvPaste(!showEnvPaste)
|
|
1080
|
+
}, (0, react.createElement)(ClipboardIcon, { size: 12 }), showEnvPaste ? " 取消" : " 批量粘贴")), showEnvPaste ? (0, react.createElement)("div", { className: "sts-env-paste" }, (0, react.createElement)("textarea", {
|
|
1081
|
+
className: "sts-env-paste-input",
|
|
1082
|
+
value: envPasteText,
|
|
1083
|
+
onChange: (e) => setEnvPasteText(e.target.value),
|
|
1084
|
+
placeholder: "粘贴 KEY=VALUE;KEY=VALUE;或以换行/逗号分隔,如:\nDB_DATABASE=cassiopeia_ap;DB_HOST=10.18.18.18;DB_PORT=3306;MQ_HOST=10.18.18.18;MQ_PASSWORD=admin123;MQ_PORT=5672;MQ_USERNAME=admin;MQ_VIRTUAL_HOST=/;REDIS_HOST=10.18.18.17;REDIS_PASSWORD=Qaz!wsx2;REDIS_PORT=6379",
|
|
1085
|
+
rows: 4
|
|
1086
|
+
}), (0, react.createElement)("div", { className: "sts-env-paste-footer" }, (0, react.createElement)("button", {
|
|
1087
|
+
className: "sts-env-paste-apply",
|
|
1088
|
+
onClick: applyEnvPaste
|
|
1089
|
+
}, "确定并生成环境变量"), (0, react.createElement)("span", { className: "sts-env-paste-hint" }, "重复将覆盖"))) : null)), (0, react.createElement)("div", { className: "sts-modal-footer" }, (0, react.createElement)("button", {
|
|
1090
|
+
className: "sts-modal-btn",
|
|
1091
|
+
onClick: onCancel
|
|
1092
|
+
}, "取消"), (0, react.createElement)("button", {
|
|
1093
|
+
className: "sts-modal-btn primary",
|
|
1094
|
+
onClick: handleSave,
|
|
1095
|
+
disabled: name.trim() === "",
|
|
1096
|
+
style: {
|
|
1097
|
+
background: "#1a1a1e",
|
|
1098
|
+
border: "1px solid #3a3a3e",
|
|
1099
|
+
color: "#fff",
|
|
1100
|
+
borderRadius: "6px",
|
|
1101
|
+
padding: "8px 18px",
|
|
1102
|
+
cursor: name.trim() === "" ? "not-allowed" : "pointer",
|
|
1103
|
+
fontSize: "13px",
|
|
1104
|
+
opacity: name.trim() === "" ? .5 : 1
|
|
1105
|
+
}
|
|
1106
|
+
}, "保存"))));
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region src/client/styles.ts
|
|
1110
|
+
/**
|
|
1111
|
+
* CSS styles for the starter plugin panel. Uses .sts- prefixed class names
|
|
1112
|
+
* (scoped enough to avoid collisions with the host shell).
|
|
1113
|
+
* Uses --dsw-alias-* CSS variables from the DSH design system to follow the
|
|
1114
|
+
* active theme automatically (dark/light).
|
|
1115
|
+
* @module dsh-better-sidebar-starter/client/styles
|
|
1116
|
+
*/
|
|
1117
|
+
const STYLES_CSS = `
|
|
1118
|
+
/* ===== Root container ===== */
|
|
1119
|
+
.sts-root {
|
|
1120
|
+
display: flex;
|
|
1121
|
+
flex-direction: column;
|
|
1122
|
+
height: 100%;
|
|
1123
|
+
min-height: 0;
|
|
1124
|
+
font-size: 13px;
|
|
1125
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1126
|
+
background: var(--dsw-alias-bg-base, #1a1a1e);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/* ===== Toolbar ===== */
|
|
1130
|
+
.sts-toolbar {
|
|
1131
|
+
display: flex;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
justify-content: space-between;
|
|
1134
|
+
padding: 0 12px;
|
|
1135
|
+
height: 40px;
|
|
1136
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1137
|
+
flex-shrink: 0;
|
|
1138
|
+
}
|
|
1139
|
+
.sts-toolbar-title {
|
|
1140
|
+
display: flex;
|
|
1141
|
+
align-items: center;
|
|
1142
|
+
gap: 8px;
|
|
1143
|
+
font: var(--dsw-font-m-14, 500 14px/20px sans-serif);
|
|
1144
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1145
|
+
}
|
|
1146
|
+
.sts-toolbar-btn {
|
|
1147
|
+
display: flex;
|
|
1148
|
+
align-items: center;
|
|
1149
|
+
gap: 4px;
|
|
1150
|
+
padding: 4px 10px;
|
|
1151
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1152
|
+
border-radius: 6px;
|
|
1153
|
+
background: transparent;
|
|
1154
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1155
|
+
cursor: pointer;
|
|
1156
|
+
font-size: 12px;
|
|
1157
|
+
transition: background 0.12s, border-color 0.12s;
|
|
1158
|
+
}
|
|
1159
|
+
.sts-toolbar-btn:hover {
|
|
1160
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1161
|
+
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1162
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/* ===== Split pane (horizontal: default for right panel) ===== */
|
|
1166
|
+
.sts-split {
|
|
1167
|
+
display: flex;
|
|
1168
|
+
flex: 1;
|
|
1169
|
+
min-height: 0;
|
|
1170
|
+
}
|
|
1171
|
+
.sts-split-first {
|
|
1172
|
+
overflow-y: auto;
|
|
1173
|
+
overflow-x: hidden;
|
|
1174
|
+
border-right: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1175
|
+
flex-shrink: 0;
|
|
1176
|
+
}
|
|
1177
|
+
.sts-split-gutter {
|
|
1178
|
+
width: 1px;
|
|
1179
|
+
background: var(--dsw-alias-border-l2, #2a2a2e);
|
|
1180
|
+
cursor: col-resize;
|
|
1181
|
+
flex-shrink: 0;
|
|
1182
|
+
transition: width 0.12s, background 0.12s;
|
|
1183
|
+
}
|
|
1184
|
+
.sts-split-gutter:hover,
|
|
1185
|
+
.sts-split-gutter.dragging {
|
|
1186
|
+
width: 3px;
|
|
1187
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, #007acc);
|
|
1188
|
+
}
|
|
1189
|
+
.sts-split-second {
|
|
1190
|
+
flex: 1;
|
|
1191
|
+
display: flex;
|
|
1192
|
+
flex-direction: column;
|
|
1193
|
+
min-width: 0;
|
|
1194
|
+
min-height: 0;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ===== Split pane (vertical: for bottom panel) ===== */
|
|
1198
|
+
.sts-split-vertical {
|
|
1199
|
+
flex-direction: column;
|
|
1200
|
+
}
|
|
1201
|
+
.sts-split-first-v {
|
|
1202
|
+
border-right: none;
|
|
1203
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1204
|
+
width: 100% !important;
|
|
1205
|
+
overflow-y: auto;
|
|
1206
|
+
overflow-x: hidden;
|
|
1207
|
+
flex-shrink: 0;
|
|
1208
|
+
}
|
|
1209
|
+
.sts-split-gutter-v {
|
|
1210
|
+
width: auto;
|
|
1211
|
+
height: 1px;
|
|
1212
|
+
cursor: row-resize;
|
|
1213
|
+
transition: height 0.12s, background 0.12s;
|
|
1214
|
+
}
|
|
1215
|
+
.sts-split-gutter-v:hover,
|
|
1216
|
+
.sts-split-gutter-v.dragging {
|
|
1217
|
+
width: auto;
|
|
1218
|
+
height: 3px;
|
|
1219
|
+
background: var(--dsw-alias-interactive-bg-hover-accent, #007acc);
|
|
1220
|
+
}
|
|
1221
|
+
.sts-split-second-v {
|
|
1222
|
+
flex: 1;
|
|
1223
|
+
display: flex;
|
|
1224
|
+
flex-direction: column;
|
|
1225
|
+
min-width: 0;
|
|
1226
|
+
min-height: 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/* ===== Config tree ===== */
|
|
1230
|
+
.sts-tree {
|
|
1231
|
+
padding: 4px 4px 8px;
|
|
1232
|
+
user-select: none;
|
|
1233
|
+
}
|
|
1234
|
+
.sts-group-header {
|
|
1235
|
+
display: flex;
|
|
1236
|
+
align-items: center;
|
|
1237
|
+
gap: 6px;
|
|
1238
|
+
padding: 6px 8px;
|
|
1239
|
+
font: var(--dsw-font-xxxs-11, 500 11px/16px sans-serif);
|
|
1240
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1241
|
+
text-transform: uppercase;
|
|
1242
|
+
letter-spacing: 0.5px;
|
|
1243
|
+
cursor: pointer;
|
|
1244
|
+
border-radius: 6px;
|
|
1245
|
+
}
|
|
1246
|
+
.sts-group-header:hover {
|
|
1247
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
|
|
1248
|
+
}
|
|
1249
|
+
.sts-group-count {
|
|
1250
|
+
color: rgba(255,255,255,0.4);
|
|
1251
|
+
font-weight: 400;
|
|
1252
|
+
font-variant-numeric: tabular-nums;
|
|
1253
|
+
}
|
|
1254
|
+
.sts-group-children {
|
|
1255
|
+
overflow: hidden;
|
|
1256
|
+
}
|
|
1257
|
+
.sts-group-children.collapsed {
|
|
1258
|
+
display: none;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/* ===== Config row ===== */
|
|
1262
|
+
.sts-config-row {
|
|
1263
|
+
display: flex;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
gap: 8px;
|
|
1266
|
+
padding: 5px 8px 5px 24px;
|
|
1267
|
+
cursor: pointer;
|
|
1268
|
+
font-size: 13px;
|
|
1269
|
+
border-radius: 6px;
|
|
1270
|
+
margin: 1px 2px;
|
|
1271
|
+
transition: background 0.1s;
|
|
1272
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1273
|
+
}
|
|
1274
|
+
.sts-config-row:hover {
|
|
1275
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
|
|
1276
|
+
}
|
|
1277
|
+
.sts-config-row.selected {
|
|
1278
|
+
background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
|
|
1279
|
+
}
|
|
1280
|
+
.sts-config-name {
|
|
1281
|
+
flex: 1;
|
|
1282
|
+
overflow: hidden;
|
|
1283
|
+
text-overflow: ellipsis;
|
|
1284
|
+
white-space: nowrap;
|
|
1285
|
+
font-weight: 400;
|
|
1286
|
+
}
|
|
1287
|
+
.sts-config-cmd {
|
|
1288
|
+
font-size: 10px;
|
|
1289
|
+
color: rgba(255,255,255,0.4);
|
|
1290
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1291
|
+
overflow: hidden;
|
|
1292
|
+
text-overflow: ellipsis;
|
|
1293
|
+
white-space: nowrap;
|
|
1294
|
+
max-width: 100px;
|
|
1295
|
+
}
|
|
1296
|
+
.sts-config-actions {
|
|
1297
|
+
display: flex;
|
|
1298
|
+
align-items: center;
|
|
1299
|
+
gap: 1px;
|
|
1300
|
+
flex-shrink: 0;
|
|
1301
|
+
}
|
|
1302
|
+
.sts-icon-btn {
|
|
1303
|
+
display: flex;
|
|
1304
|
+
align-items: center;
|
|
1305
|
+
justify-content: center;
|
|
1306
|
+
width: 22px;
|
|
1307
|
+
height: 22px;
|
|
1308
|
+
border: none;
|
|
1309
|
+
border-radius: 4px;
|
|
1310
|
+
background: transparent;
|
|
1311
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1312
|
+
cursor: pointer;
|
|
1313
|
+
opacity: 0;
|
|
1314
|
+
transition: opacity 0.1s, background 0.1s;
|
|
1315
|
+
}
|
|
1316
|
+
.sts-config-row:hover .sts-icon-btn {
|
|
1317
|
+
opacity: 0.7;
|
|
1318
|
+
}
|
|
1319
|
+
.sts-icon-btn:hover {
|
|
1320
|
+
opacity: 1 !important;
|
|
1321
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
|
|
1322
|
+
}
|
|
1323
|
+
.sts-icon-btn.play { color: #6ab04c; }
|
|
1324
|
+
.sts-icon-btn.stop { color: #eb5757; }
|
|
1325
|
+
.sts-icon-btn.gear { color: var(--dsw-alias-label-secondary, #b0b0b0); }
|
|
1326
|
+
.sts-icon-btn.trash { color: #eb5757; }
|
|
1327
|
+
|
|
1328
|
+
/* ===== Instance row ===== */
|
|
1329
|
+
.sts-instance-row {
|
|
1330
|
+
display: flex;
|
|
1331
|
+
align-items: center;
|
|
1332
|
+
gap: 8px;
|
|
1333
|
+
padding: 3px 8px 3px 40px;
|
|
1334
|
+
font-size: 11px;
|
|
1335
|
+
cursor: pointer;
|
|
1336
|
+
border-radius: 6px;
|
|
1337
|
+
margin: 1px 2px;
|
|
1338
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1339
|
+
transition: background 0.1s;
|
|
1340
|
+
}
|
|
1341
|
+
.sts-instance-row:hover {
|
|
1342
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
|
|
1343
|
+
}
|
|
1344
|
+
.sts-instance-row.selected {
|
|
1345
|
+
background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
|
|
1346
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1347
|
+
}
|
|
1348
|
+
.sts-instance-pid {
|
|
1349
|
+
color: rgba(255,255,255,0.4);
|
|
1350
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1351
|
+
font-variant-numeric: tabular-nums;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/* ===== Empty states ===== */
|
|
1355
|
+
.sts-empty {
|
|
1356
|
+
display: flex;
|
|
1357
|
+
flex-direction: column;
|
|
1358
|
+
align-items: center;
|
|
1359
|
+
justify-content: center;
|
|
1360
|
+
height: 100%;
|
|
1361
|
+
color: rgba(255,255,255,0.4);
|
|
1362
|
+
font-size: 12px;
|
|
1363
|
+
padding: 24px;
|
|
1364
|
+
text-align: center;
|
|
1365
|
+
gap: 8px;
|
|
1366
|
+
line-height: 20px;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/* ===== Log view ===== */
|
|
1370
|
+
.sts-log-toolbar {
|
|
1371
|
+
display: flex;
|
|
1372
|
+
align-items: center;
|
|
1373
|
+
gap: 2px;
|
|
1374
|
+
padding: 0 8px;
|
|
1375
|
+
height: 36px;
|
|
1376
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1377
|
+
flex-shrink: 0;
|
|
1378
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1379
|
+
font-size: 12px;
|
|
1380
|
+
}
|
|
1381
|
+
.sts-log-toolbar-spacer {
|
|
1382
|
+
flex: 1;
|
|
1383
|
+
}
|
|
1384
|
+
.sts-log-toolbar-label {
|
|
1385
|
+
display: flex;
|
|
1386
|
+
align-items: center;
|
|
1387
|
+
gap: 4px;
|
|
1388
|
+
font-size: 11px;
|
|
1389
|
+
cursor: pointer;
|
|
1390
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1391
|
+
padding: 2px 6px;
|
|
1392
|
+
border-radius: 4px;
|
|
1393
|
+
}
|
|
1394
|
+
.sts-log-toolbar-label:hover {
|
|
1395
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1396
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
|
|
1397
|
+
}
|
|
1398
|
+
.sts-log-content {
|
|
1399
|
+
flex: 1;
|
|
1400
|
+
overflow-y: auto;
|
|
1401
|
+
overflow-x: hidden;
|
|
1402
|
+
padding: 8px 12px;
|
|
1403
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1404
|
+
font-size: 12px;
|
|
1405
|
+
line-height: 1.6;
|
|
1406
|
+
white-space: pre-wrap;
|
|
1407
|
+
word-break: break-all;
|
|
1408
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1409
|
+
background: var(--dsw-alias-bg-base, #1a1a1e);
|
|
1410
|
+
}
|
|
1411
|
+
.sts-log-line {
|
|
1412
|
+
min-height: 1em;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/* ===== Config detail ===== */
|
|
1416
|
+
.sts-detail {
|
|
1417
|
+
padding: 20px;
|
|
1418
|
+
overflow-y: auto;
|
|
1419
|
+
flex: 1;
|
|
1420
|
+
}
|
|
1421
|
+
.sts-detail-card {
|
|
1422
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1423
|
+
background: var(--dsw-alias-bg-layer-2, #202024);
|
|
1424
|
+
border-radius: 12px;
|
|
1425
|
+
padding: 20px;
|
|
1426
|
+
display: flex;
|
|
1427
|
+
flex-direction: column;
|
|
1428
|
+
gap: 16px;
|
|
1429
|
+
}
|
|
1430
|
+
.sts-detail-title {
|
|
1431
|
+
display: flex;
|
|
1432
|
+
align-items: center;
|
|
1433
|
+
gap: 10px;
|
|
1434
|
+
font-size: 16px;
|
|
1435
|
+
font-weight: 500;
|
|
1436
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1437
|
+
padding-bottom: 12px;
|
|
1438
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1439
|
+
}
|
|
1440
|
+
.sts-detail-row {
|
|
1441
|
+
display: flex;
|
|
1442
|
+
gap: 12px;
|
|
1443
|
+
font-size: 13px;
|
|
1444
|
+
align-items: baseline;
|
|
1445
|
+
}
|
|
1446
|
+
.sts-detail-label {
|
|
1447
|
+
width: 72px;
|
|
1448
|
+
flex-shrink: 0;
|
|
1449
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1450
|
+
font-size: 11px;
|
|
1451
|
+
text-transform: uppercase;
|
|
1452
|
+
letter-spacing: 0.3px;
|
|
1453
|
+
}
|
|
1454
|
+
.sts-detail-value {
|
|
1455
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1456
|
+
word-break: break-all;
|
|
1457
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1458
|
+
}
|
|
1459
|
+
.sts-detail-actions {
|
|
1460
|
+
display: flex;
|
|
1461
|
+
gap: 8px;
|
|
1462
|
+
justify-content: flex-end;
|
|
1463
|
+
margin-top: 12px;
|
|
1464
|
+
padding-top: 16px;
|
|
1465
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1466
|
+
}
|
|
1467
|
+
.sts-detail-btn {
|
|
1468
|
+
display: flex;
|
|
1469
|
+
align-items: center;
|
|
1470
|
+
gap: 6px;
|
|
1471
|
+
padding: 6px 14px;
|
|
1472
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1473
|
+
border-radius: 6px;
|
|
1474
|
+
background: transparent;
|
|
1475
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1476
|
+
cursor: pointer;
|
|
1477
|
+
font-size: 12px;
|
|
1478
|
+
transition: background 0.12s, color 0.12s;
|
|
1479
|
+
}
|
|
1480
|
+
.sts-detail-btn:hover {
|
|
1481
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1482
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1483
|
+
}
|
|
1484
|
+
.sts-detail-btn.danger {
|
|
1485
|
+
color: #eb5757;
|
|
1486
|
+
border-color: rgba(235,87,87,0.4);
|
|
1487
|
+
}
|
|
1488
|
+
.sts-detail-btn.danger:hover {
|
|
1489
|
+
background: rgba(235,87,87,0.1);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/* ===== Modal ===== */
|
|
1493
|
+
.sts-modal-overlay {
|
|
1494
|
+
position: fixed;
|
|
1495
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
1496
|
+
background: rgba(0,0,0,0.45);
|
|
1497
|
+
backdrop-filter: blur(2px);
|
|
1498
|
+
display: flex;
|
|
1499
|
+
align-items: center;
|
|
1500
|
+
justify-content: center;
|
|
1501
|
+
z-index: 10000;
|
|
1502
|
+
}
|
|
1503
|
+
.sts-modal {
|
|
1504
|
+
background: var(--dsw-alias-bg-layer-3, #242428);
|
|
1505
|
+
border: 1px solid var(--dsw-alias-border-l3, #3a3a3e);
|
|
1506
|
+
border-radius: 12px;
|
|
1507
|
+
width: 480px;
|
|
1508
|
+
max-width: 90vw;
|
|
1509
|
+
max-height: 80vh;
|
|
1510
|
+
overflow-y: auto;
|
|
1511
|
+
display: flex;
|
|
1512
|
+
flex-direction: column;
|
|
1513
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
1514
|
+
}
|
|
1515
|
+
.sts-modal-header {
|
|
1516
|
+
display: flex;
|
|
1517
|
+
align-items: center;
|
|
1518
|
+
justify-content: space-between;
|
|
1519
|
+
padding: 14px 20px;
|
|
1520
|
+
font-size: 15px;
|
|
1521
|
+
font-weight: 500;
|
|
1522
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1523
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1524
|
+
}
|
|
1525
|
+
.sts-modal-body {
|
|
1526
|
+
padding: 20px;
|
|
1527
|
+
display: flex;
|
|
1528
|
+
flex-direction: column;
|
|
1529
|
+
gap: 16px;
|
|
1530
|
+
}
|
|
1531
|
+
.sts-modal-field {
|
|
1532
|
+
display: flex;
|
|
1533
|
+
flex-direction: column;
|
|
1534
|
+
gap: 6px;
|
|
1535
|
+
}
|
|
1536
|
+
.sts-modal-label {
|
|
1537
|
+
font-size: 11px;
|
|
1538
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1539
|
+
text-transform: uppercase;
|
|
1540
|
+
letter-spacing: 0.3px;
|
|
1541
|
+
}
|
|
1542
|
+
.sts-modal-input,
|
|
1543
|
+
.sts-modal-select {
|
|
1544
|
+
padding: 8px 10px;
|
|
1545
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1546
|
+
border-radius: 6px;
|
|
1547
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1548
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1549
|
+
font-size: 13px;
|
|
1550
|
+
font-family: inherit;
|
|
1551
|
+
outline: none;
|
|
1552
|
+
transition: border-color 0.12s, background 0.12s;
|
|
1553
|
+
}
|
|
1554
|
+
.sts-modal-input:focus,
|
|
1555
|
+
.sts-modal-select:focus {
|
|
1556
|
+
border-color: var(--dsw-alias-button-primary-fill, #007acc);
|
|
1557
|
+
background: var(--dsw-alias-bg-base, #1a1a1e);
|
|
1558
|
+
}
|
|
1559
|
+
.sts-modal-input.mono {
|
|
1560
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1561
|
+
}
|
|
1562
|
+
.sts-env-row {
|
|
1563
|
+
display: flex;
|
|
1564
|
+
gap: 6px;
|
|
1565
|
+
align-items: center;
|
|
1566
|
+
}
|
|
1567
|
+
.sts-env-key {
|
|
1568
|
+
flex: 1;
|
|
1569
|
+
}
|
|
1570
|
+
.sts-env-val {
|
|
1571
|
+
flex: 2;
|
|
1572
|
+
}
|
|
1573
|
+
.sts-env-sep {
|
|
1574
|
+
color: var(--dsw-alias-label-dimmed, #555);
|
|
1575
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
|
|
1576
|
+
}
|
|
1577
|
+
.sts-env-add {
|
|
1578
|
+
font-size: 12px;
|
|
1579
|
+
padding: 6px 10px;
|
|
1580
|
+
border: 1px dashed var(--dsw-alias-border-l3, #3a3a3e);
|
|
1581
|
+
border-radius: 6px;
|
|
1582
|
+
background: transparent;
|
|
1583
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1584
|
+
cursor: pointer;
|
|
1585
|
+
align-self: flex-start;
|
|
1586
|
+
transition: color 0.12s, border-color 0.12s;
|
|
1587
|
+
}
|
|
1588
|
+
.sts-env-add:hover {
|
|
1589
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1590
|
+
border-color: var(--dsw-alias-button-primary-fill, #007acc);
|
|
1591
|
+
}
|
|
1592
|
+
.sts-modal-footer {
|
|
1593
|
+
display: flex;
|
|
1594
|
+
justify-content: flex-end;
|
|
1595
|
+
gap: 8px;
|
|
1596
|
+
padding: 14px 20px;
|
|
1597
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1598
|
+
}
|
|
1599
|
+
.sts-modal-btn {
|
|
1600
|
+
padding: 8px 18px;
|
|
1601
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1602
|
+
border-radius: 6px;
|
|
1603
|
+
background: transparent;
|
|
1604
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1605
|
+
cursor: pointer;
|
|
1606
|
+
font-size: 13px;
|
|
1607
|
+
transition: background 0.12s, color 0.12s;
|
|
1608
|
+
}
|
|
1609
|
+
.sts-modal-btn:hover {
|
|
1610
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1611
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/* ===== Directory tree picker ===== */
|
|
1615
|
+
.sts-cwd-display {
|
|
1616
|
+
display: flex;
|
|
1617
|
+
align-items: center;
|
|
1618
|
+
gap: 6px;
|
|
1619
|
+
padding: 8px 10px;
|
|
1620
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1621
|
+
border-radius: 6px;
|
|
1622
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1623
|
+
cursor: pointer;
|
|
1624
|
+
transition: border-color 0.12s;
|
|
1625
|
+
font-size: 13px;
|
|
1626
|
+
}
|
|
1627
|
+
.sts-cwd-display:hover {
|
|
1628
|
+
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1629
|
+
}
|
|
1630
|
+
.sts-cwd-icon {
|
|
1631
|
+
display: flex;
|
|
1632
|
+
align-items: center;
|
|
1633
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1634
|
+
}
|
|
1635
|
+
.sts-cwd-value {
|
|
1636
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
1637
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1638
|
+
overflow: hidden;
|
|
1639
|
+
text-overflow: ellipsis;
|
|
1640
|
+
white-space: nowrap;
|
|
1641
|
+
}
|
|
1642
|
+
.sts-tree-picker-container {
|
|
1643
|
+
margin-top: 4px;
|
|
1644
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1645
|
+
border-radius: 6px;
|
|
1646
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1647
|
+
max-height: 240px;
|
|
1648
|
+
overflow-y: auto;
|
|
1649
|
+
padding: 4px;
|
|
1650
|
+
}
|
|
1651
|
+
.sts-tree-row {
|
|
1652
|
+
display: flex;
|
|
1653
|
+
align-items: center;
|
|
1654
|
+
gap: 4px;
|
|
1655
|
+
padding: 4px 8px;
|
|
1656
|
+
cursor: pointer;
|
|
1657
|
+
border-radius: 4px;
|
|
1658
|
+
font-size: 13px;
|
|
1659
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1660
|
+
transition: background 0.08s;
|
|
1661
|
+
white-space: nowrap;
|
|
1662
|
+
}
|
|
1663
|
+
.sts-tree-row:hover {
|
|
1664
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04));
|
|
1665
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1666
|
+
}
|
|
1667
|
+
.sts-tree-row.selected {
|
|
1668
|
+
background: var(--dsw-alias-interactive-bg-active, rgba(0,122,204,0.12));
|
|
1669
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1670
|
+
}
|
|
1671
|
+
.sts-tree-chevron {
|
|
1672
|
+
display: flex;
|
|
1673
|
+
align-items: center;
|
|
1674
|
+
transition: transform 0.1s;
|
|
1675
|
+
flex-shrink: 0;
|
|
1676
|
+
}
|
|
1677
|
+
.sts-tree-chevron.expanded {
|
|
1678
|
+
transform: rotate(90deg);
|
|
1679
|
+
}
|
|
1680
|
+
.sts-tree-icon {
|
|
1681
|
+
display: flex;
|
|
1682
|
+
align-items: center;
|
|
1683
|
+
flex-shrink: 0;
|
|
1684
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1685
|
+
}
|
|
1686
|
+
.sts-tree-label {
|
|
1687
|
+
overflow: hidden;
|
|
1688
|
+
text-overflow: ellipsis;
|
|
1689
|
+
}
|
|
1690
|
+
.sts-tree-loading,
|
|
1691
|
+
.sts-tree-error,
|
|
1692
|
+
.sts-tree-empty {
|
|
1693
|
+
padding: 8px 12px;
|
|
1694
|
+
font-size: 12px;
|
|
1695
|
+
color: rgba(255,255,255,0.4);
|
|
1696
|
+
}
|
|
1697
|
+
.sts-tree-error {
|
|
1698
|
+
color: #eb5757;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/* ===== Settings panel ===== */
|
|
1702
|
+
.sts-settings-panel {
|
|
1703
|
+
display: flex;
|
|
1704
|
+
flex-direction: column;
|
|
1705
|
+
gap: 4px;
|
|
1706
|
+
padding: 8px;
|
|
1707
|
+
min-width: 320px;
|
|
1708
|
+
}
|
|
1709
|
+
.sts-settings-row {
|
|
1710
|
+
padding: 4px 0;
|
|
1711
|
+
}
|
|
1712
|
+
.sts-settings-switch-row {
|
|
1713
|
+
display: flex;
|
|
1714
|
+
align-items: center;
|
|
1715
|
+
gap: 8px;
|
|
1716
|
+
cursor: pointer;
|
|
1717
|
+
font-size: 13px;
|
|
1718
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1719
|
+
}
|
|
1720
|
+
.sts-settings-switch {
|
|
1721
|
+
width: 16px;
|
|
1722
|
+
height: 16px;
|
|
1723
|
+
cursor: pointer;
|
|
1724
|
+
}
|
|
1725
|
+
.sts-settings-sep {
|
|
1726
|
+
margin: 8px 0 4px;
|
|
1727
|
+
padding: 0 4px;
|
|
1728
|
+
font-size: 10px;
|
|
1729
|
+
text-transform: uppercase;
|
|
1730
|
+
letter-spacing: 0.5px;
|
|
1731
|
+
color: rgba(255,255,255,0.4);
|
|
1732
|
+
}
|
|
1733
|
+
.sts-settings-path {
|
|
1734
|
+
display: flex;
|
|
1735
|
+
flex-direction: column;
|
|
1736
|
+
gap: 4px;
|
|
1737
|
+
padding: 4px 0;
|
|
1738
|
+
}
|
|
1739
|
+
.sts-settings-path-header {
|
|
1740
|
+
display: flex;
|
|
1741
|
+
align-items: center;
|
|
1742
|
+
justify-content: space-between;
|
|
1743
|
+
}
|
|
1744
|
+
.sts-settings-label {
|
|
1745
|
+
font-size: 12px;
|
|
1746
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1747
|
+
}
|
|
1748
|
+
.sts-settings-badge {
|
|
1749
|
+
display: flex;
|
|
1750
|
+
align-items: center;
|
|
1751
|
+
gap: 2px;
|
|
1752
|
+
font-size: 10px;
|
|
1753
|
+
padding: 1px 6px;
|
|
1754
|
+
border-radius: 10px;
|
|
1755
|
+
}
|
|
1756
|
+
.sts-settings-badge.ok {
|
|
1757
|
+
color: #4e9a06;
|
|
1758
|
+
background: rgba(78,154,6,0.12);
|
|
1759
|
+
}
|
|
1760
|
+
.sts-settings-badge.err {
|
|
1761
|
+
color: #eb5757;
|
|
1762
|
+
background: rgba(235,87,87,0.12);
|
|
1763
|
+
}
|
|
1764
|
+
.sts-settings-path-row {
|
|
1765
|
+
display: flex;
|
|
1766
|
+
align-items: center;
|
|
1767
|
+
gap: 4px;
|
|
1768
|
+
}
|
|
1769
|
+
.sts-settings-path-icon {
|
|
1770
|
+
display: flex;
|
|
1771
|
+
align-items: center;
|
|
1772
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1773
|
+
flex-shrink: 0;
|
|
1774
|
+
}
|
|
1775
|
+
.sts-settings-input {
|
|
1776
|
+
flex: 1;
|
|
1777
|
+
padding: 6px 8px;
|
|
1778
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1779
|
+
border-radius: 4px;
|
|
1780
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1781
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1782
|
+
font-size: 12px;
|
|
1783
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
1784
|
+
outline: none;
|
|
1785
|
+
min-width: 0;
|
|
1786
|
+
}
|
|
1787
|
+
.sts-settings-input:focus {
|
|
1788
|
+
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1789
|
+
}
|
|
1790
|
+
.sts-settings-input::placeholder {
|
|
1791
|
+
color: rgba(255,255,255,0.3);
|
|
1792
|
+
}
|
|
1793
|
+
.sts-settings-browse {
|
|
1794
|
+
padding: 6px 10px;
|
|
1795
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1796
|
+
border-radius: 4px;
|
|
1797
|
+
background: transparent;
|
|
1798
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1799
|
+
cursor: pointer;
|
|
1800
|
+
font-size: 12px;
|
|
1801
|
+
flex-shrink: 0;
|
|
1802
|
+
transition: background 0.12s, color 0.12s;
|
|
1803
|
+
}
|
|
1804
|
+
.sts-settings-browse:hover {
|
|
1805
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1806
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1807
|
+
}
|
|
1808
|
+
.sts-settings-msg {
|
|
1809
|
+
font-size: 10px;
|
|
1810
|
+
padding: 0 4px;
|
|
1811
|
+
color: rgba(255,255,255,0.4);
|
|
1812
|
+
}
|
|
1813
|
+
.sts-settings-msg.valid {
|
|
1814
|
+
color: #4e9a06;
|
|
1815
|
+
}
|
|
1816
|
+
.sts-settings-msg.invalid {
|
|
1817
|
+
color: #eb5757;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/* ===== Collapsible advanced section ===== */
|
|
1821
|
+
.sts-collapse-header {
|
|
1822
|
+
display: flex;
|
|
1823
|
+
align-items: center;
|
|
1824
|
+
gap: 6px;
|
|
1825
|
+
padding: 8px 10px;
|
|
1826
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1827
|
+
border-radius: 6px;
|
|
1828
|
+
cursor: pointer;
|
|
1829
|
+
font-size: 13px;
|
|
1830
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1831
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1832
|
+
transition: border-color 0.12s;
|
|
1833
|
+
user-select: none;
|
|
1834
|
+
}
|
|
1835
|
+
.sts-collapse-header:hover {
|
|
1836
|
+
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1837
|
+
}
|
|
1838
|
+
.sts-collapse-chevron {
|
|
1839
|
+
display: flex;
|
|
1840
|
+
align-items: center;
|
|
1841
|
+
transition: transform 0.12s;
|
|
1842
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1843
|
+
}
|
|
1844
|
+
.sts-collapse-chevron.expanded {
|
|
1845
|
+
transform: rotate(90deg);
|
|
1846
|
+
}
|
|
1847
|
+
.sts-collapse-hint {
|
|
1848
|
+
margin-left: auto;
|
|
1849
|
+
font-size: 10px;
|
|
1850
|
+
color: rgba(255,255,255,0.4);
|
|
1851
|
+
}
|
|
1852
|
+
.sts-collapse-body {
|
|
1853
|
+
margin-top: 8px;
|
|
1854
|
+
padding: 12px;
|
|
1855
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1856
|
+
border-radius: 6px;
|
|
1857
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1858
|
+
display: flex;
|
|
1859
|
+
flex-direction: column;
|
|
1860
|
+
gap: 10px;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/* ===== Settings picker rows & dir chooser modal ===== */
|
|
1864
|
+
.sts-settings-picker-row {
|
|
1865
|
+
margin: 6px 0;
|
|
1866
|
+
}
|
|
1867
|
+
.sts-settings-picker-row .sts-settings-label {
|
|
1868
|
+
display: block;
|
|
1869
|
+
margin-bottom: 4px;
|
|
1870
|
+
font-size: 11px;
|
|
1871
|
+
}
|
|
1872
|
+
.sts-dir-modal {
|
|
1873
|
+
background: var(--dsw-alias-bg-layer-3, #242428);
|
|
1874
|
+
border: 1px solid var(--dsw-alias-border-l3, #3a3a3e);
|
|
1875
|
+
border-radius: 12px;
|
|
1876
|
+
width: 420px;
|
|
1877
|
+
max-width: 90vw;
|
|
1878
|
+
max-height: 70vh;
|
|
1879
|
+
display: flex;
|
|
1880
|
+
flex-direction: column;
|
|
1881
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
1882
|
+
}
|
|
1883
|
+
.sts-dir-modal-tree {
|
|
1884
|
+
flex: 1;
|
|
1885
|
+
overflow-y: auto;
|
|
1886
|
+
padding: 8px;
|
|
1887
|
+
min-height: 160px;
|
|
1888
|
+
}
|
|
1889
|
+
.sts-dirtree {
|
|
1890
|
+
display: flex;
|
|
1891
|
+
flex-direction: column;
|
|
1892
|
+
gap: 2px;
|
|
1893
|
+
}
|
|
1894
|
+
.sts-dirtree-nav {
|
|
1895
|
+
display: flex;
|
|
1896
|
+
align-items: center;
|
|
1897
|
+
gap: 4px;
|
|
1898
|
+
padding: 4px 4px 8px;
|
|
1899
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1900
|
+
margin-bottom: 4px;
|
|
1901
|
+
}
|
|
1902
|
+
.sts-dirtree-up,
|
|
1903
|
+
.sts-dirtree-home {
|
|
1904
|
+
width: 24px;
|
|
1905
|
+
height: 24px;
|
|
1906
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1907
|
+
border-radius: 4px;
|
|
1908
|
+
background: transparent;
|
|
1909
|
+
color: var(--dsw-alias-label-secondary, #b0b0b0);
|
|
1910
|
+
cursor: pointer;
|
|
1911
|
+
font-size: 13px;
|
|
1912
|
+
flex-shrink: 0;
|
|
1913
|
+
transition: background 0.12s, color 0.12s;
|
|
1914
|
+
}
|
|
1915
|
+
.sts-dirtree-up:hover,
|
|
1916
|
+
.sts-dirtree-home:hover {
|
|
1917
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1918
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1919
|
+
}
|
|
1920
|
+
.sts-dirtree-up:disabled {
|
|
1921
|
+
opacity: 0.4;
|
|
1922
|
+
cursor: default;
|
|
1923
|
+
}
|
|
1924
|
+
.sts-dirtree-cur {
|
|
1925
|
+
flex: 1;
|
|
1926
|
+
font-size: 11px;
|
|
1927
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
1928
|
+
overflow: hidden;
|
|
1929
|
+
text-overflow: ellipsis;
|
|
1930
|
+
white-space: nowrap;
|
|
1931
|
+
direction: rtl;
|
|
1932
|
+
text-align: left;
|
|
1933
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
1934
|
+
}
|
|
1935
|
+
.sts-tree-select-hint {
|
|
1936
|
+
margin-left: auto;
|
|
1937
|
+
font-size: 10px;
|
|
1938
|
+
color: rgba(255,255,255,0.4);
|
|
1939
|
+
}
|
|
1940
|
+
.sts-dirtree-sub {
|
|
1941
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1942
|
+
margin-top: 2px;
|
|
1943
|
+
padding-top: 2px;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/* ===== Env bulk paste ===== */
|
|
1947
|
+
.sts-env-actions {
|
|
1948
|
+
display: flex;
|
|
1949
|
+
gap: 6px;
|
|
1950
|
+
align-items: center;
|
|
1951
|
+
margin-top: 2px;
|
|
1952
|
+
}
|
|
1953
|
+
.sts-env-paste {
|
|
1954
|
+
margin-top: 8px;
|
|
1955
|
+
display: flex;
|
|
1956
|
+
flex-direction: column;
|
|
1957
|
+
gap: 6px;
|
|
1958
|
+
}
|
|
1959
|
+
.sts-env-paste-input {
|
|
1960
|
+
width: 100%;
|
|
1961
|
+
padding: 8px 10px;
|
|
1962
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1963
|
+
border-radius: 6px;
|
|
1964
|
+
background: var(--dsw-alias-bg-layer-1, #1a1a1e);
|
|
1965
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1966
|
+
font-size: 12px;
|
|
1967
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
1968
|
+
outline: none;
|
|
1969
|
+
resize: vertical;
|
|
1970
|
+
min-height: 80px;
|
|
1971
|
+
box-sizing: border-box;
|
|
1972
|
+
}
|
|
1973
|
+
.sts-env-paste-input:focus {
|
|
1974
|
+
border-color: var(--dsw-alias-border-l3, #3a3a3e);
|
|
1975
|
+
}
|
|
1976
|
+
.sts-env-paste-input::placeholder {
|
|
1977
|
+
color: rgba(255,255,255,0.3);
|
|
1978
|
+
font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
1979
|
+
}
|
|
1980
|
+
.sts-env-paste-footer {
|
|
1981
|
+
display: flex;
|
|
1982
|
+
align-items: center;
|
|
1983
|
+
gap: 10px;
|
|
1984
|
+
}
|
|
1985
|
+
.sts-env-paste-apply {
|
|
1986
|
+
padding: 6px 14px;
|
|
1987
|
+
border: 1px solid var(--dsw-alias-border-l2, #2a2a2e);
|
|
1988
|
+
border-radius: 6px;
|
|
1989
|
+
background: transparent;
|
|
1990
|
+
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
1991
|
+
cursor: pointer;
|
|
1992
|
+
font-size: 12px;
|
|
1993
|
+
transition: background 0.12s;
|
|
1994
|
+
}
|
|
1995
|
+
.sts-env-paste-apply:hover {
|
|
1996
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06));
|
|
1997
|
+
}
|
|
1998
|
+
.sts-env-paste-hint {
|
|
1999
|
+
font-size: 10px;
|
|
2000
|
+
color: rgba(255,255,255,0.4);
|
|
2001
|
+
}
|
|
2002
|
+
`;
|
|
2003
|
+
//#endregion
|
|
2004
|
+
//#region src/client/ServicesTab.tsx
|
|
2005
|
+
/**
|
|
2006
|
+
* Services tab: the main sidebar panel component. Left 1/3 config tree,
|
|
2007
|
+
* right 2/3 log view or config detail. Manages config CRUD via the host
|
|
2008
|
+
* REST API and tracks running instances.
|
|
2009
|
+
* @module dsh-better-sidebar-starter/client/ServicesTab
|
|
2010
|
+
*/
|
|
2011
|
+
/** API base. */
|
|
2012
|
+
const API_BASE = "/api/dsh-better-sidebar-starter";
|
|
2013
|
+
/** Instance poll interval (ms). */
|
|
2014
|
+
const POLL_INTERVAL = 3e3;
|
|
2015
|
+
/** Inject styles once. */
|
|
2016
|
+
let STYLES_INJECTED = false;
|
|
2017
|
+
function injectStyles() {
|
|
2018
|
+
if (STYLES_INJECTED) return;
|
|
2019
|
+
STYLES_INJECTED = true;
|
|
2020
|
+
const tag = document.createElement("style");
|
|
2021
|
+
tag.id = "dsh-better-sidebar-starter-styles";
|
|
2022
|
+
tag.setAttribute("data-plugin", "@dong-victor/dsh-better-sidebar-starter");
|
|
2023
|
+
tag.textContent = STYLES_CSS;
|
|
2024
|
+
document.head.appendChild(tag);
|
|
2025
|
+
}
|
|
2026
|
+
/** The main services tab component. */
|
|
2027
|
+
function ServicesTab(props) {
|
|
2028
|
+
injectStyles();
|
|
2029
|
+
const { scope, store } = props;
|
|
2030
|
+
const [configs, setConfigs] = (0, react.useState)([]);
|
|
2031
|
+
const [instances, setInstances] = (0, react.useState)([]);
|
|
2032
|
+
const [selectedConfigId, setSelectedConfigId] = (0, react.useState)(null);
|
|
2033
|
+
const [selectedInstanceId, setSelectedInstanceId] = (0, react.useState)(null);
|
|
2034
|
+
const [editingConfig, setEditingConfig] = (0, react.useState)(null);
|
|
2035
|
+
const [showModal, setShowModal] = (0, react.useState)(false);
|
|
2036
|
+
const [splitRatio, setSplitRatio] = (0, react.useState)(.33);
|
|
2037
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
2038
|
+
const [isHorizontal, setIsHorizontal] = (0, react.useState)(true);
|
|
2039
|
+
const containerRef = (0, react.useRef)(null);
|
|
2040
|
+
(0, react.useEffect)(() => {
|
|
2041
|
+
const el = containerRef.current;
|
|
2042
|
+
if (el === null) return;
|
|
2043
|
+
const check = () => {
|
|
2044
|
+
const rect = el.getBoundingClientRect();
|
|
2045
|
+
setIsHorizontal(rect.width > rect.height * 1.2);
|
|
2046
|
+
};
|
|
2047
|
+
check();
|
|
2048
|
+
const ro = new ResizeObserver(check);
|
|
2049
|
+
ro.observe(el);
|
|
2050
|
+
return () => ro.disconnect();
|
|
2051
|
+
}, []);
|
|
2052
|
+
(0, react.useEffect)(() => {
|
|
2053
|
+
const blob = store.getPrefs()?.pluginSettings?.["dsh-better-sidebar-starter:services"] ?? {};
|
|
2054
|
+
if (typeof blob.splitRatio === "number") setSplitRatio(Math.max(.2, Math.min(.5, blob.splitRatio)));
|
|
2055
|
+
}, [store]);
|
|
2056
|
+
/** Persist split ratio to plugin settings. */
|
|
2057
|
+
const persistSplitRatio = (0, react.useCallback)((ratio) => {
|
|
2058
|
+
const service = store.service;
|
|
2059
|
+
if (service !== void 0 && typeof service.updateTab === "function") {}
|
|
2060
|
+
}, [store]);
|
|
2061
|
+
/** Build a query string for the session scope. */
|
|
2062
|
+
const sessionQuery = (0, react.useCallback)(() => {
|
|
2063
|
+
const params = new URLSearchParams({ sessionId: scope.sessionId });
|
|
2064
|
+
if (scope.cwd !== void 0) params.set("cwd", scope.cwd);
|
|
2065
|
+
return params.toString();
|
|
2066
|
+
}, [scope]);
|
|
2067
|
+
/** Fetch configs from the host. */
|
|
2068
|
+
const fetchConfigs = (0, react.useCallback)(async () => {
|
|
2069
|
+
try {
|
|
2070
|
+
const resp = await fetch(`${API_BASE}/configs?${sessionQuery()}`);
|
|
2071
|
+
if (resp.ok) {
|
|
2072
|
+
const data = await resp.json();
|
|
2073
|
+
setConfigs(data.configs);
|
|
2074
|
+
}
|
|
2075
|
+
} catch {}
|
|
2076
|
+
}, [sessionQuery]);
|
|
2077
|
+
/** Fetch instances from the host. */
|
|
2078
|
+
const fetchInstances = (0, react.useCallback)(async () => {
|
|
2079
|
+
try {
|
|
2080
|
+
const resp = await fetch(`${API_BASE}/instances?${sessionQuery()}`);
|
|
2081
|
+
if (resp.ok) {
|
|
2082
|
+
const data = await resp.json();
|
|
2083
|
+
setInstances(data.instances);
|
|
2084
|
+
}
|
|
2085
|
+
} catch {}
|
|
2086
|
+
}, [sessionQuery]);
|
|
2087
|
+
(0, react.useEffect)(() => {
|
|
2088
|
+
fetchConfigs();
|
|
2089
|
+
fetchInstances();
|
|
2090
|
+
}, [fetchConfigs, fetchInstances]);
|
|
2091
|
+
(0, react.useEffect)(() => {
|
|
2092
|
+
const timer = setInterval(() => void fetchInstances(), POLL_INTERVAL);
|
|
2093
|
+
return () => clearInterval(timer);
|
|
2094
|
+
}, [fetchInstances]);
|
|
2095
|
+
/** Start a config. */
|
|
2096
|
+
const handleStart = (0, react.useCallback)(async (configId) => {
|
|
2097
|
+
try {
|
|
2098
|
+
const blob = store.getPrefs()?.pluginSettings?.["dsh-better-sidebar-starter:services"] ?? {};
|
|
2099
|
+
const globalEnv = {};
|
|
2100
|
+
if (typeof blob.javaHome === "string" && blob.javaHome.trim() !== "") globalEnv.JAVA_HOME = blob.javaHome.trim();
|
|
2101
|
+
if (typeof blob.nodePath === "string" && blob.nodePath.trim() !== "") globalEnv.NODE_PATH = blob.nodePath.trim();
|
|
2102
|
+
if (typeof blob.pythonPath === "string" && blob.pythonPath.trim() !== "") globalEnv.PYTHON_PATH = blob.pythonPath.trim();
|
|
2103
|
+
if (typeof blob.mvnPath === "string" && blob.mvnPath.trim() !== "") globalEnv.MVN_PATH = blob.mvnPath.trim();
|
|
2104
|
+
const resp = await fetch(`${API_BASE}/run?${sessionQuery()}`, {
|
|
2105
|
+
method: "POST",
|
|
2106
|
+
headers: { "content-type": "application/json" },
|
|
2107
|
+
body: JSON.stringify({
|
|
2108
|
+
configId,
|
|
2109
|
+
globalEnv
|
|
2110
|
+
})
|
|
2111
|
+
});
|
|
2112
|
+
if (resp.ok) {
|
|
2113
|
+
const data = await resp.json();
|
|
2114
|
+
setSelectedInstanceId(data.instance.id);
|
|
2115
|
+
setSelectedConfigId(null);
|
|
2116
|
+
fetchInstances();
|
|
2117
|
+
fetchConfigs();
|
|
2118
|
+
}
|
|
2119
|
+
} catch {}
|
|
2120
|
+
}, [
|
|
2121
|
+
sessionQuery,
|
|
2122
|
+
fetchInstances,
|
|
2123
|
+
fetchConfigs,
|
|
2124
|
+
store
|
|
2125
|
+
]);
|
|
2126
|
+
/** Stop an instance. */
|
|
2127
|
+
const handleStop = (0, react.useCallback)(async (instanceId) => {
|
|
2128
|
+
try {
|
|
2129
|
+
await fetch(`${API_BASE}/stop`, {
|
|
2130
|
+
method: "POST",
|
|
2131
|
+
headers: { "content-type": "application/json" },
|
|
2132
|
+
body: JSON.stringify({ instanceId })
|
|
2133
|
+
});
|
|
2134
|
+
fetchInstances();
|
|
2135
|
+
} catch {}
|
|
2136
|
+
}, [fetchInstances]);
|
|
2137
|
+
/** Restart = stop + start the same config. */
|
|
2138
|
+
const handleRestart = (0, react.useCallback)(async (instanceId) => {
|
|
2139
|
+
const inst = instances.find((i) => i.id === instanceId);
|
|
2140
|
+
if (inst === void 0) return;
|
|
2141
|
+
await handleStop(instanceId);
|
|
2142
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2143
|
+
await handleStart(inst.configId);
|
|
2144
|
+
}, [
|
|
2145
|
+
instances,
|
|
2146
|
+
handleStop,
|
|
2147
|
+
handleStart
|
|
2148
|
+
]);
|
|
2149
|
+
/** Open the new config modal. */
|
|
2150
|
+
const handleNewConfig = () => {
|
|
2151
|
+
setEditingConfig(null);
|
|
2152
|
+
setShowModal(true);
|
|
2153
|
+
};
|
|
2154
|
+
/** Open the edit config modal. */
|
|
2155
|
+
const handleEditConfig = (configId) => {
|
|
2156
|
+
const config = configs.find((c) => c.id === configId);
|
|
2157
|
+
if (config === void 0) return;
|
|
2158
|
+
setEditingConfig({
|
|
2159
|
+
id: config.id,
|
|
2160
|
+
name: config.name,
|
|
2161
|
+
type: config.type,
|
|
2162
|
+
command: config.command,
|
|
2163
|
+
cwd: config.cwd,
|
|
2164
|
+
env: config.env,
|
|
2165
|
+
jvmArgs: config.jvmArgs,
|
|
2166
|
+
args: config.args,
|
|
2167
|
+
runtime: config.runtime
|
|
2168
|
+
});
|
|
2169
|
+
setShowModal(true);
|
|
2170
|
+
};
|
|
2171
|
+
/** Save a config (create or update). */
|
|
2172
|
+
const handleSaveConfig = (0, react.useCallback)(async (config) => {
|
|
2173
|
+
try {
|
|
2174
|
+
if ((await fetch(`${API_BASE}/configs?${sessionQuery()}`, {
|
|
2175
|
+
method: "POST",
|
|
2176
|
+
headers: { "content-type": "application/json" },
|
|
2177
|
+
body: JSON.stringify({ config })
|
|
2178
|
+
})).ok) {
|
|
2179
|
+
setShowModal(false);
|
|
2180
|
+
setEditingConfig(null);
|
|
2181
|
+
fetchConfigs();
|
|
2182
|
+
}
|
|
2183
|
+
} catch {}
|
|
2184
|
+
}, [sessionQuery, fetchConfigs]);
|
|
2185
|
+
/** Delete a config. */
|
|
2186
|
+
const handleDeleteConfig = (0, react.useCallback)(async (configId) => {
|
|
2187
|
+
for (const inst of instances) if (inst.configId === configId && inst.status === "running") await handleStop(inst.id);
|
|
2188
|
+
try {
|
|
2189
|
+
if ((await fetch(`${API_BASE}/configs?${sessionQuery()}&id=${encodeURIComponent(configId)}`, { method: "DELETE" })).ok) {
|
|
2190
|
+
if (selectedConfigId === configId) setSelectedConfigId(null);
|
|
2191
|
+
fetchConfigs();
|
|
2192
|
+
fetchInstances();
|
|
2193
|
+
}
|
|
2194
|
+
} catch {}
|
|
2195
|
+
}, [
|
|
2196
|
+
instances,
|
|
2197
|
+
handleStop,
|
|
2198
|
+
sessionQuery,
|
|
2199
|
+
fetchConfigs,
|
|
2200
|
+
fetchInstances,
|
|
2201
|
+
selectedConfigId
|
|
2202
|
+
]);
|
|
2203
|
+
/** Select a config (deselect any instance). */
|
|
2204
|
+
const handleSelectConfig = (configId) => {
|
|
2205
|
+
setSelectedConfigId(configId);
|
|
2206
|
+
setSelectedInstanceId(null);
|
|
2207
|
+
};
|
|
2208
|
+
/** Select an instance. */
|
|
2209
|
+
const handleSelectInstance = (instanceId) => {
|
|
2210
|
+
setSelectedInstanceId(instanceId);
|
|
2211
|
+
setSelectedConfigId(null);
|
|
2212
|
+
};
|
|
2213
|
+
const handleMouseDown = (e) => {
|
|
2214
|
+
e.preventDefault();
|
|
2215
|
+
setDragging(true);
|
|
2216
|
+
};
|
|
2217
|
+
(0, react.useEffect)(() => {
|
|
2218
|
+
if (!dragging) return;
|
|
2219
|
+
const handleMove = (e) => {
|
|
2220
|
+
if (containerRef.current === null) return;
|
|
2221
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2222
|
+
const ratio = isHorizontal ? (e.clientX - rect.left) / rect.width : (e.clientY - rect.top) / rect.height;
|
|
2223
|
+
const clamped = Math.max(.2, Math.min(.5, ratio));
|
|
2224
|
+
setSplitRatio(clamped);
|
|
2225
|
+
persistSplitRatio(clamped);
|
|
2226
|
+
};
|
|
2227
|
+
const handleUp = () => setDragging(false);
|
|
2228
|
+
document.addEventListener("mousemove", handleMove);
|
|
2229
|
+
document.addEventListener("mouseup", handleUp);
|
|
2230
|
+
return () => {
|
|
2231
|
+
document.removeEventListener("mousemove", handleMove);
|
|
2232
|
+
document.removeEventListener("mouseup", handleUp);
|
|
2233
|
+
};
|
|
2234
|
+
}, [
|
|
2235
|
+
dragging,
|
|
2236
|
+
isHorizontal,
|
|
2237
|
+
persistSplitRatio
|
|
2238
|
+
]);
|
|
2239
|
+
const selectedInstance = selectedInstanceId !== null ? instances.find((i) => i.id === selectedInstanceId) ?? null : null;
|
|
2240
|
+
const selectedConfig = selectedConfigId !== null ? configs.find((c) => c.id === selectedConfigId) ?? null : null;
|
|
2241
|
+
return (0, react.createElement)("div", {
|
|
2242
|
+
ref: containerRef,
|
|
2243
|
+
className: "sts-root"
|
|
2244
|
+
}, (0, react.createElement)("div", { className: "sts-toolbar" }, (0, react.createElement)("div", { className: "sts-toolbar-title" }, (0, react.createElement)(RocketIcon, { size: 16 }), "服务"), (0, react.createElement)("button", {
|
|
2245
|
+
className: "sts-toolbar-btn",
|
|
2246
|
+
onClick: handleNewConfig
|
|
2247
|
+
}, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
|
|
2248
|
+
className: `sts-split-first${isHorizontal ? "" : " sts-split-first-v"}`,
|
|
2249
|
+
style: isHorizontal ? { width: `${splitRatio * 100}%` } : { height: `${splitRatio * 100}%` }
|
|
2250
|
+
}, configs.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置,点击右上角 + 新建") : (0, react.createElement)(ConfigTree, {
|
|
2251
|
+
configs,
|
|
2252
|
+
instances,
|
|
2253
|
+
selectedInstanceId,
|
|
2254
|
+
selectedConfigId,
|
|
2255
|
+
onSelectConfig: handleSelectConfig,
|
|
2256
|
+
onSelectInstance: handleSelectInstance,
|
|
2257
|
+
onStartConfig: (id) => {
|
|
2258
|
+
handleStart(id);
|
|
2259
|
+
},
|
|
2260
|
+
onStopInstance: (id) => {
|
|
2261
|
+
handleStop(id);
|
|
2262
|
+
},
|
|
2263
|
+
onEditConfig: handleEditConfig
|
|
2264
|
+
})), (0, react.createElement)("div", {
|
|
2265
|
+
className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
|
|
2266
|
+
onMouseDown: handleMouseDown
|
|
2267
|
+
}), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, selectedInstance !== null ? (0, react.createElement)(LogView, {
|
|
2268
|
+
instance: selectedInstance,
|
|
2269
|
+
onStop: () => {
|
|
2270
|
+
handleStop(selectedInstance.id);
|
|
2271
|
+
},
|
|
2272
|
+
onRestart: () => {
|
|
2273
|
+
handleRestart(selectedInstance.id);
|
|
2274
|
+
}
|
|
2275
|
+
}) : selectedConfig !== null ? (0, react.createElement)(ConfigDetail, {
|
|
2276
|
+
config: selectedConfig,
|
|
2277
|
+
onStart: () => {
|
|
2278
|
+
handleStart(selectedConfig.id);
|
|
2279
|
+
},
|
|
2280
|
+
onEdit: () => handleEditConfig(selectedConfig.id),
|
|
2281
|
+
onDelete: () => {
|
|
2282
|
+
handleDeleteConfig(selectedConfig.id);
|
|
2283
|
+
}
|
|
2284
|
+
}) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看详情或日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
|
|
2285
|
+
config: editingConfig,
|
|
2286
|
+
scope: {
|
|
2287
|
+
sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
|
|
2288
|
+
cwd: scope.cwd
|
|
2289
|
+
},
|
|
2290
|
+
onSave: (c) => {
|
|
2291
|
+
handleSaveConfig(c);
|
|
2292
|
+
},
|
|
2293
|
+
onCancel: () => {
|
|
2294
|
+
setShowModal(false);
|
|
2295
|
+
setEditingConfig(null);
|
|
2296
|
+
}
|
|
2297
|
+
}) : null);
|
|
2298
|
+
}
|
|
2299
|
+
//#endregion
|
|
2300
|
+
//#region src/client/SettingsPanel.tsx
|
|
2301
|
+
/**
|
|
2302
|
+
* Custom settings panel for the "服务" tab: renders switch toggles plus
|
|
2303
|
+
* validated directory pickers for Java/Node/Python/Maven paths.
|
|
2304
|
+
* @module dsh-better-sidebar-starter/client/SettingsPanel
|
|
2305
|
+
*/
|
|
2306
|
+
/** The custom settings panel rendered in the gear popup. */
|
|
2307
|
+
function SettingsPanel(props) {
|
|
2308
|
+
const { store, pluginSettings, updatePluginSetting } = props;
|
|
2309
|
+
const scope = { sessionId: store.getSnapshot().sessionId ?? "" };
|
|
2310
|
+
const get = (key) => typeof pluginSettings[key] === "string" ? pluginSettings[key] : "";
|
|
2311
|
+
const getBool = (key) => pluginSettings[key] === true;
|
|
2312
|
+
const [autoSelect, setAutoSelect] = (0, react.useState)(getBool("autoSelectOnStart"));
|
|
2313
|
+
const [autoScroll, setAutoScroll] = (0, react.useState)(getBool("autoScroll"));
|
|
2314
|
+
const [javaHome, setJavaHome] = (0, react.useState)(get("javaHome"));
|
|
2315
|
+
const [nodePath, setNodePath] = (0, react.useState)(get("nodePath"));
|
|
2316
|
+
const [pythonPath, setPythonPath] = (0, react.useState)(get("pythonPath"));
|
|
2317
|
+
const [mvnPath, setMvnPath] = (0, react.useState)(get("mvnPath"));
|
|
2318
|
+
(0, react.useEffect)(() => {
|
|
2319
|
+
if (!(get("javaHome") === "" && get("nodePath") === "" && get("pythonPath") === "" && get("mvnPath") === "")) return;
|
|
2320
|
+
let cancelled = false;
|
|
2321
|
+
fetch("/api/dsh-better-sidebar-starter/detect-env").then((r) => r.ok ? r.json() : null).then((detected) => {
|
|
2322
|
+
if (cancelled || detected === null) return;
|
|
2323
|
+
if (detected.javaHome !== void 0 && detected.javaHome !== "") {
|
|
2324
|
+
setJavaHome(detected.javaHome);
|
|
2325
|
+
updatePluginSetting("javaHome", detected.javaHome);
|
|
2326
|
+
}
|
|
2327
|
+
if (detected.nodePath !== void 0 && detected.nodePath !== "") {
|
|
2328
|
+
setNodePath(detected.nodePath);
|
|
2329
|
+
updatePluginSetting("nodePath", detected.nodePath);
|
|
2330
|
+
}
|
|
2331
|
+
if (detected.pythonPath !== void 0 && detected.pythonPath !== "") {
|
|
2332
|
+
setPythonPath(detected.pythonPath);
|
|
2333
|
+
updatePluginSetting("pythonPath", detected.pythonPath);
|
|
2334
|
+
}
|
|
2335
|
+
if (detected.mvnPath !== void 0 && detected.mvnPath !== "") {
|
|
2336
|
+
setMvnPath(detected.mvnPath);
|
|
2337
|
+
updatePluginSetting("mvnPath", detected.mvnPath);
|
|
2338
|
+
}
|
|
2339
|
+
}).catch(() => {});
|
|
2340
|
+
return () => {
|
|
2341
|
+
cancelled = true;
|
|
2342
|
+
};
|
|
2343
|
+
}, []);
|
|
2344
|
+
const handleSwitch = (key, setter) => (v) => {
|
|
2345
|
+
setter(v);
|
|
2346
|
+
updatePluginSetting(key, v);
|
|
2347
|
+
};
|
|
2348
|
+
const handlePath = (key, setter) => (v) => {
|
|
2349
|
+
setter(v);
|
|
2350
|
+
updatePluginSetting(key, v);
|
|
2351
|
+
};
|
|
2352
|
+
return (0, react.createElement)("div", { className: "sts-settings-panel" }, (0, react.createElement)("div", { className: "sts-settings-row" }, (0, react.createElement)("label", { className: "sts-settings-switch-row" }, (0, react.createElement)("input", {
|
|
2353
|
+
type: "checkbox",
|
|
2354
|
+
className: "sts-settings-switch",
|
|
2355
|
+
checked: autoSelect,
|
|
2356
|
+
onChange: (e) => handleSwitch("autoSelectOnStart", setAutoSelect)(e.target.checked)
|
|
2357
|
+
}), (0, react.createElement)("span", null, "启动后自动选中日志"))), (0, react.createElement)("div", { className: "sts-settings-row" }, (0, react.createElement)("label", { className: "sts-settings-switch-row" }, (0, react.createElement)("input", {
|
|
2358
|
+
type: "checkbox",
|
|
2359
|
+
className: "sts-settings-switch",
|
|
2360
|
+
checked: autoScroll,
|
|
2361
|
+
onChange: (e) => handleSwitch("autoScroll", setAutoScroll)(e.target.checked)
|
|
2362
|
+
}), (0, react.createElement)("span", null, "自动滚动到最新日志"))), (0, react.createElement)("div", { className: "sts-settings-sep" }, "运行环境路径"), (0, react.createElement)(PathPickerRow, {
|
|
2363
|
+
label: "Java 主目录 (JAVA_HOME)",
|
|
2364
|
+
scope,
|
|
2365
|
+
kind: "java",
|
|
2366
|
+
value: javaHome,
|
|
2367
|
+
onChange: handlePath("javaHome", setJavaHome),
|
|
2368
|
+
desc: "Spring Boot 用的 JDK,留空则自动查找"
|
|
2369
|
+
}), (0, react.createElement)(PathPickerRow, {
|
|
2370
|
+
label: "Node.js 路径",
|
|
2371
|
+
scope,
|
|
2372
|
+
kind: "node",
|
|
2373
|
+
value: nodePath,
|
|
2374
|
+
onChange: handlePath("nodePath", setNodePath),
|
|
2375
|
+
desc: "npm 用的 Node,留空则用系统默认"
|
|
2376
|
+
}), (0, react.createElement)(PathPickerRow, {
|
|
2377
|
+
label: "Python 路径",
|
|
2378
|
+
scope,
|
|
2379
|
+
kind: "python",
|
|
2380
|
+
value: pythonPath,
|
|
2381
|
+
onChange: handlePath("pythonPath", setPythonPath),
|
|
2382
|
+
desc: "Python 解释器,留空则用系统默认"
|
|
2383
|
+
}), (0, react.createElement)(PathPickerRow, {
|
|
2384
|
+
label: "Maven 路径 (MVN)",
|
|
2385
|
+
scope,
|
|
2386
|
+
kind: "mvn",
|
|
2387
|
+
value: mvnPath,
|
|
2388
|
+
onChange: handlePath("mvnPath", setMvnPath),
|
|
2389
|
+
desc: "springboot 用的 Maven,留空则自动查找"
|
|
2390
|
+
}));
|
|
2391
|
+
}
|
|
2392
|
+
/** A labeled path picker row for the settings panel. */
|
|
2393
|
+
function PathPickerRow(props) {
|
|
2394
|
+
return (0, react.createElement)("div", { className: "sts-settings-picker-row" }, (0, react.createElement)("label", { className: "sts-settings-label" }, props.label), (0, react.createElement)(PathPicker, {
|
|
2395
|
+
scope: props.scope,
|
|
2396
|
+
kind: props.kind,
|
|
2397
|
+
value: props.value,
|
|
2398
|
+
onChange: props.onChange,
|
|
2399
|
+
desc: props.desc
|
|
2400
|
+
}));
|
|
2401
|
+
}
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/client/index.tsx
|
|
2404
|
+
/**
|
|
2405
|
+
* Client half of the dsh-better-sidebar-starter plugin: registers the
|
|
2406
|
+
* "服务" (Services) sidebar tab through the `ctx.betterSidebar` service.
|
|
2407
|
+
* The tab renders a split-pane panel: left config tree + right log/detail.
|
|
2408
|
+
* The host half (same package) owns the session-scoped config/process routes.
|
|
2409
|
+
* @module dsh-better-sidebar-starter/client
|
|
2410
|
+
*/
|
|
2411
|
+
/** Services required before mounting: the betterSidebar registry (provided
|
|
2412
|
+
* by dsh-better-sidebar's own client half) and the session store. */
|
|
2413
|
+
const inject = ["betterSidebar", "sessions"];
|
|
2414
|
+
/** Plugin body. */
|
|
2415
|
+
function apply(ctx) {
|
|
2416
|
+
ctx.effect(() => ctx.betterSidebar.registerTab({
|
|
2417
|
+
id: "dsh-better-sidebar-starter:services",
|
|
2418
|
+
title: () => "服务",
|
|
2419
|
+
icon: (size) => (0, react.createElement)(RocketIcon, { size }),
|
|
2420
|
+
order: 50,
|
|
2421
|
+
single: true,
|
|
2422
|
+
settings: {
|
|
2423
|
+
pluginToggles: [
|
|
2424
|
+
{
|
|
2425
|
+
key: "autoSelectOnStart",
|
|
2426
|
+
type: "switch",
|
|
2427
|
+
title: "启动后自动选中日志"
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
key: "autoScroll",
|
|
2431
|
+
type: "switch",
|
|
2432
|
+
title: "自动滚动到最新日志"
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
key: "javaHome",
|
|
2436
|
+
type: "text",
|
|
2437
|
+
title: "Java 主目录 (JAVA_HOME)",
|
|
2438
|
+
desc: "Spring Boot 用的 JDK 路径,留空则自动查找"
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
key: "nodePath",
|
|
2442
|
+
type: "text",
|
|
2443
|
+
title: "Node.js 路径",
|
|
2444
|
+
desc: "npm 命令用的 Node 路径,留空则用系统默认"
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
key: "pythonPath",
|
|
2448
|
+
type: "text",
|
|
2449
|
+
title: "Python 路径",
|
|
2450
|
+
desc: "Python 命令用的解释器路径,留空则用系统默认"
|
|
2451
|
+
}
|
|
2452
|
+
],
|
|
2453
|
+
render: (props) => (0, react.createElement)(SettingsPanel, props)
|
|
2454
|
+
},
|
|
2455
|
+
component: (props) => (0, react.createElement)(ServicesTab, props)
|
|
2456
|
+
}), "dsh-better-sidebar-starter: services tab");
|
|
2457
|
+
}
|
|
2458
|
+
//#endregion
|
|
2459
|
+
exports.apply = apply;
|
|
2460
|
+
exports.inject = inject;
|
|
2461
|
+
return module.exports;
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
//# sourceMappingURL=client.js.map
|