@dong-victor/dsh-better-sidebar-starter 0.1.3 → 0.1.5

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/lib/client.js CHANGED
@@ -143,7 +143,7 @@ window.__ModuleLoader__.load({
143
143
  "python",
144
144
  "custom"
145
145
  ];
146
- const TYPE_LABELS$2 = {
146
+ const TYPE_LABELS$1 = {
147
147
  npm: "npm",
148
148
  springboot: "Spring Boot",
149
149
  python: "Python",
@@ -187,7 +187,7 @@ window.__ModuleLoader__.load({
187
187
  if (configs.length === 0) return (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置", (0, react.createElement)("br"), "点击右上角 + 新建配置");
188
188
  return (0, react.createElement)("div", { className: "sts-tree" }, ...groups.map((group) => {
189
189
  const isCollapsed = collapsed.has(group.type);
190
- const label = TYPE_LABELS$2[group.type] ?? group.type;
190
+ const label = TYPE_LABELS$1[group.type] ?? group.type;
191
191
  const runningCount = instances.filter((inst) => inst.configType === group.type && inst.status === "running").length;
192
192
  return (0, react.createElement)("div", { key: group.type }, (0, react.createElement)("div", {
193
193
  className: "sts-group-header",
@@ -628,6 +628,31 @@ window.__ModuleLoader__.load({
628
628
  /** Render ANSI-colored text segments, applying Java-log syntax highlighting
629
629
  * to text the program did not color itself (ANSI-colored spans keep the
630
630
  * program's own colors). Tokens with a source link become clickable. */
631
+ /** Split plain text into plain/url parts. Trailing punctuation that is
632
+ * almost certainly sentence text (.,;:)])。,) stays outside the link. */
633
+ const URL_RE = /https?:\/\/[^\s"'<>()\[\]{}]+/g;
634
+ function splitUrlParts(text) {
635
+ const out = [];
636
+ let last = 0;
637
+ for (const m of text.matchAll(URL_RE)) {
638
+ const idx = m.index ?? 0;
639
+ if (idx > last) out.push({ text: text.slice(last, idx) });
640
+ let url = m[0];
641
+ let trail = "";
642
+ while (url.length > 0 && /[.,;:)\]}》」),。;:]$/.test(url)) {
643
+ trail = url.slice(-1) + trail;
644
+ url = url.slice(0, -1);
645
+ }
646
+ if (url !== "") out.push({
647
+ text: url,
648
+ url
649
+ });
650
+ if (trail !== "") out.push({ text: trail });
651
+ last = idx + m[0].length;
652
+ }
653
+ if (last < text.length) out.push({ text: text.slice(last) });
654
+ return out;
655
+ }
631
656
  function renderAnsiLine(text, keyBase, onLinkClick) {
632
657
  const segments = parseAnsi(text);
633
658
  const nodes = [];
@@ -649,8 +674,28 @@ window.__ModuleLoader__.load({
649
674
  key: `${keyBase}-${i}`,
650
675
  style
651
676
  }, seg.text));
652
- } else {
653
- const hl = highlightLogText(seg.text);
677
+ } else for (const part of splitUrlParts(seg.text)) {
678
+ if (part.url !== void 0) {
679
+ const url = part.url;
680
+ nodes.push((0, react.createElement)("span", {
681
+ key: `${keyBase}-${i}-u${hlCounter++}`,
682
+ className: "sts-log-url",
683
+ style: {
684
+ ...baseStyle,
685
+ textDecoration: "underline",
686
+ cursor: "pointer"
687
+ },
688
+ title: `在浏览器打开 ${url}`,
689
+ onClick: (e) => {
690
+ e.stopPropagation();
691
+ try {
692
+ window.open(url, "_blank", "noopener");
693
+ } catch {}
694
+ }
695
+ }, part.url));
696
+ continue;
697
+ }
698
+ const hl = highlightLogText(part.text);
654
699
  for (const h of hl) {
655
700
  const style = { ...baseStyle };
656
701
  if (h.color !== void 0) style.color = h.color;
@@ -870,7 +915,7 @@ window.__ModuleLoader__.load({
870
915
  if (parts.length > 1 && parts[parts.length - 1] === "") pushLine();
871
916
  }
872
917
  /** The log history viewer. */
873
- function LogHistoryView({ ctx, sessionId, cwd, onClose }) {
918
+ function LogHistoryView({ ctx, sessionId, cwd, onClose, filter }) {
874
919
  const [files, setFiles] = (0, react.useState)(null);
875
920
  const [loading, setLoading] = (0, react.useState)(false);
876
921
  const [activeFile, setActiveFile] = (0, react.useState)(null);
@@ -884,7 +929,7 @@ window.__ModuleLoader__.load({
884
929
  if (cwd !== void 0 && cwd !== "") params.set("cwd", cwd);
885
930
  const resp = await fetch(`${API_BASE$1}/logs-history?${params.toString()}`);
886
931
  if (!resp.ok) {
887
- let reason = "加载历史日志失败";
932
+ let reason = filter !== void 0 ? "加载历史日志失败" : "加载历史日志失败";
888
933
  try {
889
934
  const err = await resp.json();
890
935
  if (typeof err.error === "string" && err.error !== "") reason = err.error;
@@ -893,15 +938,20 @@ window.__ModuleLoader__.load({
893
938
  setFiles([]);
894
939
  return;
895
940
  }
896
- const data = await resp.json();
897
- setFiles(data.logs ?? []);
941
+ const all = (await resp.json()).logs ?? [];
942
+ const shown = filter !== void 0 ? all.filter((f) => f.name.startsWith(filter)) : all;
943
+ setFiles(shown);
898
944
  } catch {
899
945
  showToast("加载历史日志失败", false);
900
946
  setFiles([]);
901
947
  } finally {
902
948
  setLoading(false);
903
949
  }
904
- }, [sessionId, cwd]);
950
+ }, [
951
+ sessionId,
952
+ cwd,
953
+ filter
954
+ ]);
905
955
  const [initialized, setInitialized] = (0, react.useState)(false);
906
956
  if (!initialized) {
907
957
  setInitialized(true);
@@ -941,40 +991,43 @@ window.__ModuleLoader__.load({
941
991
  setLoading(false);
942
992
  }
943
993
  }, [sessionId, cwd]);
944
- if (activeFile === null) return (0, react.createElement)("div", { style: {
945
- display: "flex",
946
- flexDirection: "column",
947
- height: "100%",
948
- minHeight: 0
949
- } }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, "历史日志", (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
950
- className: "sts-icon-btn",
951
- title: "刷新",
952
- onClick: () => {
953
- loadList();
954
- }
955
- }, RefreshIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "刷新")), (0, react.createElement)("button", {
956
- className: "sts-icon-btn",
957
- title: "关闭历史日志",
958
- onClick: onClose
959
- }, CloseIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "关闭"))), (0, react.createElement)("div", { style: {
960
- overflow: "auto",
961
- flex: 1,
962
- minHeight: 0,
963
- padding: "4px 0"
964
- } }, loading && files === null ? (0, react.createElement)("div", { className: "sts-empty" }, "加载中…") : files !== null && files.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无历史日志(工作空间 .dsh/logs 为空)。每次启动服务都会在 <工作空间>/.dsh/logs 下永久保留一份日志,即使启动失败或实例被清理也能查看。") : (0, react.createElement)("div", {}, (files ?? []).map((f) => (0, react.createElement)("button", {
965
- key: f.file,
966
- className: "sts-history-item",
967
- title: f.file,
968
- onClick: () => {
969
- openFile(f);
970
- }
971
- }, (0, react.createElement)(FileIcon, { size: 13 }), (0, react.createElement)("span", { style: {
972
- flex: 1,
973
- textAlign: "left",
974
- overflow: "hidden",
975
- textOverflow: "ellipsis",
976
- whiteSpace: "nowrap"
977
- } }, f.name), (0, react.createElement)("span", { className: "sts-history-item-meta" }, `${new Date(f.mtime).toLocaleString()} ${(f.size / 1024).toFixed(1)} KB`))))));
994
+ if (activeFile === null) {
995
+ const title = filter !== void 0 ? `${filter} · 历史日志` : "历史日志";
996
+ return (0, react.createElement)("div", { style: {
997
+ display: "flex",
998
+ flexDirection: "column",
999
+ height: "100%",
1000
+ minHeight: 0
1001
+ } }, (0, react.createElement)("div", { className: "sts-log-toolbar" }, title, (0, react.createElement)("div", { className: "sts-log-toolbar-spacer" }), (0, react.createElement)("button", {
1002
+ className: "sts-icon-btn",
1003
+ title: "刷新",
1004
+ onClick: () => {
1005
+ loadList();
1006
+ }
1007
+ }, RefreshIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, "刷新")), (0, react.createElement)("button", {
1008
+ className: "sts-icon-btn",
1009
+ title: filter !== void 0 ? "返回服务视图" : "关闭历史日志",
1010
+ onClick: onClose
1011
+ }, CloseIcon({ size: 14 }), (0, react.createElement)("span", { className: "sts-icon-btn-label" }, filter !== void 0 ? "返回" : "关闭"))), (0, react.createElement)("div", { style: {
1012
+ overflow: "auto",
1013
+ flex: 1,
1014
+ minHeight: 0,
1015
+ padding: "4px 0"
1016
+ } }, loading && files === null ? (0, react.createElement)("div", { className: "sts-empty" }, "加载中…") : files !== null && files.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, filter !== void 0 ? `「${filter}」暂无历史日志。每次启动都会在 <工作空间>/logs/ 下永久保留一份日志,即使启动失败或 dsh 重启也能查看。` : "暂无历史日志(工作空间 logs/ 为空)。每次启动服务都会在 <工作空间>/logs/ 下永久保留一份日志,即使启动失败或 dsh 重启也能查看。") : (0, react.createElement)("div", {}, (files ?? []).map((f) => (0, react.createElement)("button", {
1017
+ key: f.file,
1018
+ className: "sts-history-item",
1019
+ title: f.file,
1020
+ onClick: () => {
1021
+ openFile(f);
1022
+ }
1023
+ }, (0, react.createElement)(FileIcon, { size: 13 }), (0, react.createElement)("span", { style: {
1024
+ flex: 1,
1025
+ textAlign: "left",
1026
+ overflow: "hidden",
1027
+ textOverflow: "ellipsis",
1028
+ whiteSpace: "nowrap"
1029
+ } }, f.name), (0, react.createElement)("span", { className: "sts-history-item-meta" }, `${new Date(f.mtime).toLocaleString()} ${(f.size / 1024).toFixed(1)} KB`))))));
1030
+ }
978
1031
  return (0, react.createElement)("div", { style: {
979
1032
  display: "flex",
980
1033
  flexDirection: "column",
@@ -1009,53 +1062,6 @@ window.__ModuleLoader__.load({
1009
1062
  }, `—— 历史日志已加载完毕(${lines.length} 行),此为只读视图 ——`)));
1010
1063
  }
1011
1064
  //#endregion
1012
- //#region src/client/ConfigDetail.tsx
1013
- /**
1014
- * Config detail card: shown in the right pane when an idle config is selected.
1015
- * Displays the config's full info and offers start / edit / delete buttons.
1016
- * @module dsh-better-sidebar-starter/client/ConfigDetail
1017
- */
1018
- const TYPE_LABELS$1 = {
1019
- npm: "npm",
1020
- springboot: "Spring Boot",
1021
- python: "Python",
1022
- custom: "Custom"
1023
- };
1024
- /** The detail card component. */
1025
- function ConfigDetail({ config, onStart, onEdit, onDuplicate, onDelete }) {
1026
- const envEntries = Object.entries(config.env);
1027
- const dateFmt = (ts) => {
1028
- if (ts === null) return "—";
1029
- return new Date(ts).toLocaleString("zh-CN", { hour12: false });
1030
- };
1031
- 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", {
1032
- key,
1033
- className: "sts-detail-value"
1034
- }, `${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", {
1035
- className: "sts-detail-btn danger",
1036
- onClick: onDelete
1037
- }, (0, react.createElement)(TrashIcon, { size: 14 }), " 删除"), (0, react.createElement)("button", {
1038
- className: "sts-detail-btn",
1039
- onClick: onDuplicate,
1040
- title: "复制为副本(保留全部信息)"
1041
- }, (0, react.createElement)(CopyIcon, { size: 14 }), " 复制"), (0, react.createElement)("button", {
1042
- className: "sts-detail-btn",
1043
- onClick: onEdit
1044
- }, (0, react.createElement)(GearIcon, { size: 14 }), " 编辑"), (0, react.createElement)("button", {
1045
- className: "sts-detail-btn primary",
1046
- onClick: onStart,
1047
- style: {
1048
- background: "#1a1a1e",
1049
- border: "1px solid #3a3a3e",
1050
- color: "#fff",
1051
- borderRadius: "6px",
1052
- padding: "6px 14px",
1053
- cursor: "pointer",
1054
- fontSize: "12px"
1055
- }
1056
- }, (0, react.createElement)(PlayIcon, { size: 14 }), " 启动"))));
1057
- }
1058
- //#endregion
1059
1065
  //#region src/client/DirChooser.tsx
1060
1066
  /**
1061
1067
  * Shared directory chooser: a filesystem folder tree (backed by the sidebar
@@ -2021,6 +2027,13 @@ window.__ModuleLoader__.load({
2021
2027
  background: rgba(97, 175, 239, 0.14);
2022
2028
  border-bottom-color: #61afef;
2023
2029
  }
2030
+ /* Clickable http(s) URLs inside the log (open in a new browser tab). */
2031
+ .sts-log-url {
2032
+ color: #61afef;
2033
+ }
2034
+ .sts-log-url:hover {
2035
+ background: rgba(97, 175, 239, 0.18);
2036
+ }
2024
2037
 
2025
2038
  /* ===== Config detail ===== */
2026
2039
  .sts-detail {
@@ -2646,7 +2659,6 @@ window.__ModuleLoader__.load({
2646
2659
  const [splitRatio, setSplitRatio] = (0, react.useState)(.33);
2647
2660
  const [dragging, setDragging] = (0, react.useState)(false);
2648
2661
  const [isHorizontal, setIsHorizontal] = (0, react.useState)(true);
2649
- const [historyOpen, setHistoryOpen] = (0, react.useState)(false);
2650
2662
  const containerRef = (0, react.useRef)(null);
2651
2663
  (0, react.useEffect)(() => {
2652
2664
  const el = containerRef.current;
@@ -2821,8 +2833,7 @@ window.__ModuleLoader__.load({
2821
2833
  }
2822
2834
  } catch {}
2823
2835
  }, [sessionQuery, fetchConfigs]);
2824
- /** Delete a config. */
2825
- const handleDeleteConfig = (0, react.useCallback)(async (configId) => {
2836
+ (0, react.useCallback)(async (configId) => {
2826
2837
  for (const inst of instances) if (inst.configId === configId && inst.status === "running") await handleStop(inst.id);
2827
2838
  try {
2828
2839
  if ((await fetch(`${API_BASE}/configs?${sessionQuery()}&id=${encodeURIComponent(configId)}`, { method: "DELETE" })).ok) {
@@ -2883,14 +2894,7 @@ window.__ModuleLoader__.load({
2883
2894
  }, (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", {
2884
2895
  className: "sts-toolbar-btn",
2885
2896
  onClick: handleNewConfig
2886
- }, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置"), (0, react.createElement)("button", {
2887
- className: `sts-toolbar-btn${historyOpen ? " active" : ""}`,
2888
- title: "查看持久化历史日志(.dsh/logs,含启动失败记录)",
2889
- onClick: () => {
2890
- setHistoryOpen((v) => !v);
2891
- if (historyOpen) {}
2892
- }
2893
- }, (0, react.createElement)(FileIcon, { size: 13 }), "历史日志")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
2897
+ }, (0, react.createElement)(PlusIcon, { size: 13 }), "新建配置")), (0, react.createElement)("div", { className: `sts-split${isHorizontal ? "" : " sts-split-vertical"}` }, (0, react.createElement)("div", {
2894
2898
  className: `sts-split-first${isHorizontal ? "" : " sts-split-first-v"}`,
2895
2899
  style: isHorizontal ? { width: `${splitRatio * 100}%` } : { height: `${splitRatio * 100}%` }
2896
2900
  }, configs.length === 0 ? (0, react.createElement)("div", { className: "sts-empty" }, "暂无运行配置,点击右上角 + 新建") : (0, react.createElement)(ConfigTree, {
@@ -2911,12 +2915,7 @@ window.__ModuleLoader__.load({
2911
2915
  })), (0, react.createElement)("div", {
2912
2916
  className: `sts-split-gutter${dragging ? " dragging" : ""}${isHorizontal ? "" : " sts-split-gutter-v"}`,
2913
2917
  onMouseDown: handleMouseDown
2914
- }), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, historyOpen ? (0, react.createElement)(LogHistoryView, {
2915
- ctx: props.ctx,
2916
- sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
2917
- cwd: scope.cwd,
2918
- onClose: () => setHistoryOpen(false)
2919
- }) : selectedInstance !== null ? (0, react.createElement)(LogView, {
2918
+ }), (0, react.createElement)("div", { className: `sts-split-second${isHorizontal ? "" : " sts-split-second-v"}` }, selectedInstance !== null ? (0, react.createElement)(LogView, {
2920
2919
  instance: selectedInstance,
2921
2920
  ctx: props.ctx,
2922
2921
  sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
@@ -2927,17 +2926,15 @@ window.__ModuleLoader__.load({
2927
2926
  onRestart: () => {
2928
2927
  handleRestart(selectedInstance.id);
2929
2928
  }
2930
- }) : selectedConfig !== null ? (0, react.createElement)(ConfigDetail, {
2931
- config: selectedConfig,
2932
- onStart: () => {
2933
- handleStart(selectedConfig.id);
2934
- },
2935
- onEdit: () => handleEditConfig(selectedConfig.id),
2936
- onDuplicate: () => handleDuplicateConfig(selectedConfig.id),
2937
- onDelete: () => {
2938
- handleDeleteConfig(selectedConfig.id);
2929
+ }) : selectedConfig !== null ? (0, react.createElement)(LogHistoryView, {
2930
+ ctx: props.ctx,
2931
+ sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",
2932
+ cwd: scope.cwd,
2933
+ filter: selectedConfig.name,
2934
+ onClose: () => {
2935
+ setSelectedConfigId(null);
2939
2936
  }
2940
- }) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看详情或日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
2937
+ }) : (0, react.createElement)("div", { className: "sts-empty" }, "选择左侧配置查看其历史日志,或选择运行中的服务查看实时日志"))), showModal ? (0, react.createElement)(ConfigEditorModal, {
2941
2938
  config: editingConfig,
2942
2939
  scope: {
2943
2940
  sessionId: scope.sessionId ?? store.getSnapshot().sessionId ?? "",