mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 669762a9c9 | |||
| f975ecce48 | |||
| a121bd4bb5 | |||
| dcc7b2943e |
@@ -26,7 +26,7 @@ import * as appconst from "@/app/appconst";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
|
||||
import "./screenview.less";
|
||||
// import "./tabs.less";
|
||||
import "./tabs.less";
|
||||
import { MagicLayout } from "../../magiclayout";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
.screen-tab {
|
||||
font: var(--base-font);
|
||||
font-size: var(--screentabs-font-size);
|
||||
line-height: var(--screentabs-line-height);
|
||||
border-top: 2px solid transparent;
|
||||
background: var(--app-bg-color);
|
||||
.background {
|
||||
// This applies a transparency mask to the background color, as set above, so that it will blend with whatever the theme's background color is.
|
||||
z-index: 1;
|
||||
width: var(--screen-tab-width);
|
||||
mask-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
font-weight: var(--screentabs-selected-font-weight);
|
||||
border-top: 2px solid var(--tab-color);
|
||||
}
|
||||
&.is-archived {
|
||||
.fa.fa-archive {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
svg.svg-icon-inner path {
|
||||
fill: var(--tab-color);
|
||||
}
|
||||
.tabicon i {
|
||||
color: var(--tab-color);
|
||||
}
|
||||
&.color-green,
|
||||
&.color-default {
|
||||
--tab-color: var(--tab-green);
|
||||
}
|
||||
&.color-orange {
|
||||
--tab-color: var(--tab-orange);
|
||||
}
|
||||
&.color-red {
|
||||
--tab-color: var(--tab-red);
|
||||
}
|
||||
&.color-yellow {
|
||||
--tab-color: var(--tab-yellow);
|
||||
}
|
||||
&.color-blue {
|
||||
--tab-color: var(--tab-blue);
|
||||
}
|
||||
&.color-mint {
|
||||
--tab-color: var(--tab-mint);
|
||||
}
|
||||
&.color-cyan {
|
||||
--tab-color: var(--tab-cyan);
|
||||
}
|
||||
&.color-white {
|
||||
--tab-color: var(--tab-white);
|
||||
}
|
||||
&.color-violet {
|
||||
--tab-color: var(--tab-violet);
|
||||
}
|
||||
&.color-pink {
|
||||
--tab-color: var(--tab-pink);
|
||||
}
|
||||
.screen-tab-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
min-width: var(--screen-tab-width);
|
||||
max-width: var(--screen-tab-width);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 8px 4px 8px; // extra 4px of tab padding to account for horizontal scrollbar (to make tab text look centered)
|
||||
.front-icon {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Only one of these will be visible at a time
|
||||
.end-icons {
|
||||
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
|
||||
margin: 0 -5px 0 0;
|
||||
line-height: normal;
|
||||
.tab-index {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.vertical-line {
|
||||
border-left: 1px solid var(--app-border-color);
|
||||
margin: 10px 0 8px 0;
|
||||
}
|
||||
&:not(:hover) .status-indicator {
|
||||
.status-indicator-visible;
|
||||
}
|
||||
&:hover {
|
||||
.actions {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useRef } from "react";
|
||||
import cn from "classnames";
|
||||
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
|
||||
import { TabIcon } from "@/elements/tabicon";
|
||||
import { GlobalModel, Screen } from "@/models";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import * as constants from "@/app/appconst";
|
||||
|
||||
import "./tab2.less";
|
||||
|
||||
type ScreenTabProps = {
|
||||
screen: Screen;
|
||||
activeScreenId: string;
|
||||
onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void;
|
||||
onSwitchScreen: (screenId: string) => void;
|
||||
};
|
||||
|
||||
const ScreenTab: React.FC<ScreenTabProps> = mobxReact.observer(
|
||||
({ screen, activeScreenId, onSwitchScreen, onDragStart }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const openScreenSettings = (e: any, screen: Screen): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mobx.action(() => {
|
||||
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
|
||||
})();
|
||||
GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
|
||||
};
|
||||
|
||||
const archived = screen.archived.get() ? (
|
||||
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
|
||||
) : null;
|
||||
const statusIndicatorLevel = screen.statusIndicator.get();
|
||||
const runningCommands = screen.numRunningCmds.get() > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-screenid={screen.screenId}
|
||||
className={cn(
|
||||
"screen-tab",
|
||||
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
|
||||
"color-" + screen.getTabColor()
|
||||
)}
|
||||
onMouseDown={() => onDragStart(screen.name.get(), ref)}
|
||||
onClick={() => onSwitchScreen(screen.screenId)}
|
||||
data-screentab-name={screen.name.get()}
|
||||
>
|
||||
<div className="background"></div>
|
||||
<div className="screen-tab-inner">
|
||||
<CenteredIcon className="front-icon">
|
||||
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
|
||||
</CenteredIcon>
|
||||
<div className="tab-name truncate">
|
||||
{archived}
|
||||
{screen.name.get()}
|
||||
</div>
|
||||
<div className="end-icons">
|
||||
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
|
||||
<ActionsIcon onClick={(e) => openScreenSettings(e, screen)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="vertical-line"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { ScreenTab };
|
||||
@@ -1,45 +0,0 @@
|
||||
.screen-tabs-container {
|
||||
position: relative;
|
||||
height: var(--screentabs-height);
|
||||
|
||||
.screen-tabs-container-inner {
|
||||
position: relative; // Needed for absolute positioning of child tabs
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
margin-right: 42px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-screen {
|
||||
width: 42px;
|
||||
height: 40px;
|
||||
background-color: var(--app-bg-color);
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
|
||||
// More margin is given on macOS to account for the traffic light buttons
|
||||
#main.platform-darwin.mainsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-logo-width-darwin);
|
||||
}
|
||||
|
||||
#main:not(.platform-darwin).mainsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-logo-width);
|
||||
}
|
||||
|
||||
// This ensures the tab bar does not collide with the right sidebar triggers.
|
||||
#main.platform-darwin.rightsidebar-collapsed .screen-tabs-container {
|
||||
margin-right: var(--floating-right-sidebar-triggers-width-darwin);
|
||||
}
|
||||
|
||||
#main:not(.platform-darwin).rightsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-right-sidebar-triggers-width);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { ScreenTab } from "./tab2";
|
||||
import { observer, useLocalObservable } from "mobx-react";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
|
||||
import AddIcon from "@/assets/icons/add.svg";
|
||||
|
||||
import "./tabs2.less";
|
||||
|
||||
const DEFAULT_TAB_WIDTH = 170;
|
||||
|
||||
type ScreenTabsProps = {
|
||||
session: Session;
|
||||
};
|
||||
|
||||
const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
|
||||
const [screens, setScreens] = useState<Screen[]>([]);
|
||||
const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH);
|
||||
const [_, setDraggedTab] = useState<string | null>(null);
|
||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||
const tabContainerRef = useRef<HTMLDivElement>(null);
|
||||
const addBtnRef = useRef<HTMLDivElement>(null);
|
||||
const mainSidebarWidth = GlobalModel.mainSidebarModel.getWidth();
|
||||
const rightSidebarWidth = GlobalModel.rightSidebarModel.getWidth();
|
||||
let prevDelta: number;
|
||||
let prevDragDirection: string;
|
||||
let draggedRemoved: boolean;
|
||||
let shrunk: boolean;
|
||||
|
||||
const store = useLocalObservable(() => ({
|
||||
get activeScreenId() {
|
||||
return session?.activeScreenId.get();
|
||||
},
|
||||
get screens() {
|
||||
let activeScreenId = store.activeScreenId;
|
||||
if (!activeScreenId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let screens = GlobalModel.getSessionScreens(session.sessionId);
|
||||
let filteredScreens = screens.filter(
|
||||
(screen) => !screen.archived.get() || activeScreenId === screen.screenId
|
||||
);
|
||||
|
||||
filteredScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get());
|
||||
return filteredScreens;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Update tabs when screens change
|
||||
const dispose = reaction(
|
||||
() => store.screens,
|
||||
(screens) => {
|
||||
setScreens(screens);
|
||||
}
|
||||
);
|
||||
// Clean up
|
||||
return () => {
|
||||
if (dispose) dispose();
|
||||
};
|
||||
}, [screens.length]);
|
||||
|
||||
const getActiveScreenId = (): string | null => {
|
||||
if (session) {
|
||||
return session.activeScreenId.get();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateTabPositions = useCallback(() => {
|
||||
if (tabContainerRef.current) {
|
||||
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
|
||||
let newStartPositions = [];
|
||||
let cumulativeLeft = 0; // Start from the left edge
|
||||
|
||||
tabElements.forEach((tab) => {
|
||||
newStartPositions.push(cumulativeLeft);
|
||||
cumulativeLeft += tab.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
||||
});
|
||||
|
||||
setDragStartPositions(newStartPositions);
|
||||
}
|
||||
}, [screens]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTabPositions();
|
||||
}, [screens, updateTabPositions]);
|
||||
|
||||
const resizeTabs = useCallback(() => {
|
||||
if (tabContainerRef.current) {
|
||||
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
|
||||
const numberOfTabs = screens.length;
|
||||
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||||
|
||||
if (totalDefaultTabWidth > containerWidth) {
|
||||
// Case where resizing is needed due to limited container width
|
||||
shrunk = true;
|
||||
const newTabWidth = containerWidth / numberOfTabs;
|
||||
setTabWidth(newTabWidth);
|
||||
screens.forEach((screen, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${screen.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
tabElement.style.width = `${newTabWidth}px`;
|
||||
tabElement.style.left = `${index * newTabWidth}px`;
|
||||
});
|
||||
} else if (shrunk || totalDefaultTabWidth < containerWidth) {
|
||||
// Case where tabs were previously shrunk or there is enough space for default width tabs
|
||||
shrunk = false;
|
||||
setTabWidth(DEFAULT_TAB_WIDTH);
|
||||
screens.forEach((screen, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${screen.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`;
|
||||
tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`;
|
||||
});
|
||||
}
|
||||
|
||||
// Update the position of the Add Tab button
|
||||
const addButtonElement = addBtnRef.current;
|
||||
if (addButtonElement && tabContainerRef.current) {
|
||||
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
|
||||
const lastTab = tabElements[tabElements.length - 1];
|
||||
|
||||
if (lastTab) {
|
||||
const lastTabRect = lastTab.getBoundingClientRect();
|
||||
const containerRect = tabContainerRef.current.getBoundingClientRect();
|
||||
|
||||
// Calculate the left position relative to the tab container
|
||||
addButtonElement.style.left = `${lastTabRect.right - containerRect.left}px`;
|
||||
}
|
||||
}
|
||||
updateTabPositions();
|
||||
}
|
||||
}, [screens.length, updateTabPositions]);
|
||||
|
||||
// Resize tabs when the number of tabs or the window size changes
|
||||
useEffect(() => {
|
||||
resizeTabs();
|
||||
window.addEventListener("resize", resizeTabs);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeTabs);
|
||||
};
|
||||
}, [resizeTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
resizeTabs();
|
||||
}, [mainSidebarWidth, rightSidebarWidth]);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
(screenId: string, ref: React.RefObject<HTMLDivElement>) => {
|
||||
setDraggedTab(screenId);
|
||||
let tabIndex = screens.findIndex((screen) => screen.screenId === screenId);
|
||||
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
||||
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
|
||||
|
||||
if (ref.current) {
|
||||
let initialOffsetX: number | null = null;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (initialOffsetX === null) {
|
||||
initialOffsetX = event.clientX - tabStartX;
|
||||
}
|
||||
let currentX = event.clientX - initialOffsetX;
|
||||
|
||||
// Constrain movement within the container bounds
|
||||
if (tabContainerRef.current) {
|
||||
const numberOfTabs = screens.length;
|
||||
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||||
const containerRect = tabContainerRef.current.getBoundingClientRect();
|
||||
let containerRectWidth = containerRect.width;
|
||||
// Set to the total default tab width if there's vacant space
|
||||
if (totalDefaultTabWidth < containerRectWidth) {
|
||||
containerRectWidth = totalDefaultTabWidth;
|
||||
}
|
||||
|
||||
const minLeft = 0;
|
||||
const maxRight = containerRectWidth - tabWidth;
|
||||
|
||||
// Adjust currentX to stay within bounds
|
||||
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
|
||||
}
|
||||
|
||||
ref.current.style.transform = `translateX(${currentX - tabStartX}px)`;
|
||||
ref.current.style.zIndex = "100";
|
||||
|
||||
let dragDirection;
|
||||
if (currentX - prevDelta > 0) {
|
||||
dragDirection = "+";
|
||||
} else if (currentX - prevDelta === 0) {
|
||||
dragDirection = prevDragDirection;
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
prevDelta = currentX;
|
||||
prevDragDirection = dragDirection;
|
||||
|
||||
let newTabIndex = tabIndex;
|
||||
|
||||
if (dragDirection === "+") {
|
||||
// Dragging to the right
|
||||
for (let i = tabIndex + 1; i < screens.length; i++) {
|
||||
const otherTabStart = dragStartPositions[i];
|
||||
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
|
||||
newTabIndex = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dragging to the left
|
||||
for (let i = tabIndex - 1; i >= 0; i--) {
|
||||
const otherTabEnd = dragStartPositions[i] + tabWidth;
|
||||
if (currentX < otherTabEnd - tabWidth / 2) {
|
||||
newTabIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rearrange the tabs temporarily
|
||||
if (newTabIndex !== tabIndex) {
|
||||
const tempTabs = Array.from(screens);
|
||||
|
||||
// Remove the dragged tab if not already done
|
||||
if (!draggedRemoved) {
|
||||
screens.splice(tabIndex, 1);
|
||||
draggedRemoved = true;
|
||||
}
|
||||
|
||||
// Find current index of the dragged tab in tempTabs
|
||||
const currentIndexOfDraggedTab = tabs.indexOf(name);
|
||||
|
||||
// Move the dragged tab to its new position
|
||||
if (currentIndexOfDraggedTab !== -1) {
|
||||
tabs.splice(currentIndexOfDraggedTab, 1);
|
||||
}
|
||||
tabs.splice(newTabIndex, 0, name);
|
||||
|
||||
// Update visual positions of the tabs
|
||||
tabs.forEach((tempTab, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${tempTab}"]`
|
||||
) as HTMLElement;
|
||||
if (tempTab !== name) {
|
||||
tabElement.style.left = `${index * tabWidth}px`;
|
||||
}
|
||||
});
|
||||
|
||||
tabIndex = newTabIndex;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (ref.current) {
|
||||
// Reset transform for all tabs
|
||||
const tabElements = tabContainerRef.current.querySelectorAll(".screen-tab");
|
||||
tabElements.forEach((tab) => {
|
||||
const htmlTab = tab as HTMLElement;
|
||||
htmlTab.style.transform = "";
|
||||
htmlTab.style.zIndex = "0";
|
||||
});
|
||||
|
||||
// Update the final position of the dragged tab
|
||||
const draggedTab = screens[tabIndex];
|
||||
const finalLeftPosition = tabIndex * tabWidth;
|
||||
const draggedTabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${draggedTab.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
if (draggedTabElement) {
|
||||
draggedTabElement.style.left = `${finalLeftPosition}px`;
|
||||
}
|
||||
}
|
||||
|
||||
setDraggedTab(null);
|
||||
draggedRemoved = false;
|
||||
};
|
||||
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
},
|
||||
[screens, dragStartPositions]
|
||||
);
|
||||
|
||||
const onSwitchScreen = (screenId: string) => {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (session.activeScreenId.get() == screenId) {
|
||||
return;
|
||||
}
|
||||
let screen = session.getScreenById(screenId);
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
GlobalCommandRunner.switchScreen(screenId);
|
||||
};
|
||||
|
||||
const handleNewScreen = () => {
|
||||
GlobalCommandRunner.createNewScreen();
|
||||
};
|
||||
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
const screen: Screen | null = null;
|
||||
const activeScreenId = getActiveScreenId();
|
||||
|
||||
return (
|
||||
<div className="screen-tabs-container">
|
||||
<div className="screen-tabs-container-inner" ref={tabContainerRef}>
|
||||
<For each="screen" of={tabs}>
|
||||
<ScreenTab
|
||||
key={screen.screenId}
|
||||
screen={screen}
|
||||
activeScreenId={activeScreenId}
|
||||
onSwitchScreen={onSwitchScreen}
|
||||
onDragStart={onDragStart}
|
||||
/>
|
||||
</For>
|
||||
</div>
|
||||
<div ref={addBtnRef} className="new-screen" onClick={handleNewScreen} style={{ left: DEFAULT_TAB_WIDTH }}>
|
||||
<AddIcon className="icon hoverEffect" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { ScreenTabs };
|
||||
@@ -11,7 +11,7 @@ import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { CmdInput } from "./cmdinput/cmdinput";
|
||||
import { ScreenView } from "./screen/screenview";
|
||||
import { ScreenTabs } from "./screen/tabs2";
|
||||
import { ScreenTabs } from "./screen/tabs";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
import "./workspace.less";
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scws"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/wsshell"
|
||||
)
|
||||
|
||||
@@ -211,7 +212,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
|
||||
return
|
||||
}
|
||||
activity := sstore.ActivityUpdate{}
|
||||
activity := telemetry.ActivityUpdate{}
|
||||
if activeState.Fg {
|
||||
activity.FgMinutes = 1
|
||||
}
|
||||
@@ -222,7 +223,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
|
||||
activity.OpenMinutes = 1
|
||||
}
|
||||
activity.NumConns = remote.NumRemotes()
|
||||
err = sstore.UpdateCurrentActivity(r.Context(), activity)
|
||||
err = telemetry.UpdateCurrentActivity(r.Context(), activity)
|
||||
if err != nil {
|
||||
WriteJsonError(w, fmt.Errorf("error updating activity: %w", err))
|
||||
return
|
||||
@@ -998,7 +999,7 @@ func main() {
|
||||
}
|
||||
|
||||
log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint())
|
||||
sstore.UpdateActivityWrap(context.Background(), sstore.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
|
||||
telemetry.UpdateActivityWrap(context.Background(), telemetry.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
|
||||
installSignalHandlers()
|
||||
go telemetryLoop()
|
||||
go stdinReadWatch()
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bookmarks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
)
|
||||
|
||||
type BookmarkType struct {
|
||||
BookmarkId string `json:"bookmarkid"`
|
||||
CreatedTs int64 `json:"createdts"`
|
||||
CmdStr string `json:"cmdstr"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
Description string `json:"description"`
|
||||
OrderIdx int64 `json:"orderidx"`
|
||||
Remove bool `json:"remove,omitempty"`
|
||||
}
|
||||
|
||||
func (bm *BookmarkType) GetSimpleKey() string {
|
||||
return bm.BookmarkId
|
||||
}
|
||||
|
||||
func (bm *BookmarkType) ToMap() map[string]interface{} {
|
||||
rtn := make(map[string]interface{})
|
||||
rtn["bookmarkid"] = bm.BookmarkId
|
||||
rtn["createdts"] = bm.CreatedTs
|
||||
rtn["cmdstr"] = bm.CmdStr
|
||||
rtn["alias"] = bm.Alias
|
||||
rtn["description"] = bm.Description
|
||||
rtn["tags"] = dbutil.QuickJsonArr(bm.Tags)
|
||||
return rtn
|
||||
}
|
||||
|
||||
func (bm *BookmarkType) FromMap(m map[string]interface{}) bool {
|
||||
dbutil.QuickSetStr(&bm.BookmarkId, m, "bookmarkid")
|
||||
dbutil.QuickSetInt64(&bm.CreatedTs, m, "createdts")
|
||||
dbutil.QuickSetStr(&bm.Alias, m, "alias")
|
||||
dbutil.QuickSetStr(&bm.CmdStr, m, "cmdstr")
|
||||
dbutil.QuickSetStr(&bm.Description, m, "description")
|
||||
dbutil.QuickSetJsonArr(&bm.Tags, m, "tags")
|
||||
return true
|
||||
}
|
||||
|
||||
type bookmarkOrderType struct {
|
||||
BookmarkId string
|
||||
OrderIdx int64
|
||||
}
|
||||
|
||||
func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) {
|
||||
var bms []*BookmarkType
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
var query string
|
||||
if tag == "" {
|
||||
query = `SELECT * FROM bookmark`
|
||||
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query)
|
||||
} else {
|
||||
query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)`
|
||||
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query, tag)
|
||||
}
|
||||
bmMap := dbutil.MakeGenMap(bms)
|
||||
var orders []bookmarkOrderType
|
||||
query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?`
|
||||
tx.Select(&orders, query, tag)
|
||||
for _, bmOrder := range orders {
|
||||
bm := bmMap[bmOrder.BookmarkId]
|
||||
if bm != nil {
|
||||
bm.OrderIdx = bmOrder.OrderIdx
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
}
|
||||
return bms, nil
|
||||
}
|
||||
|
||||
func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*BookmarkType, error) {
|
||||
var rtn *BookmarkType
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `SELECT * FROM bookmark WHERE bookmarkid = ?`
|
||||
rtn = dbutil.GetMapGen[*BookmarkType](tx, query, bookmarkId)
|
||||
if rtn == nil {
|
||||
return nil
|
||||
}
|
||||
query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?`
|
||||
orderIdx := tx.GetInt(query, bookmarkId, tag)
|
||||
rtn.OrderIdx = int64(orderIdx)
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func GetBookmarkIdByArg(ctx context.Context, bookmarkArg string) (string, error) {
|
||||
var rtnId string
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
if len(bookmarkArg) == 8 {
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid LIKE (? || '%')`
|
||||
rtnId = tx.GetString(query, bookmarkArg)
|
||||
return nil
|
||||
}
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
|
||||
rtnId = tx.GetString(query, bookmarkArg)
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return "", txErr
|
||||
}
|
||||
return rtnId, nil
|
||||
}
|
||||
|
||||
func GetBookmarkIdsByCmdStr(ctx context.Context, cmdStr string) ([]string, error) {
|
||||
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) ([]string, error) {
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE cmdstr = ?`
|
||||
bmIds := tx.SelectStrings(query, cmdStr)
|
||||
return bmIds, nil
|
||||
})
|
||||
}
|
||||
|
||||
// ignores OrderIdx field
|
||||
func InsertBookmark(ctx context.Context, bm *BookmarkType) error {
|
||||
if bm == nil || bm.BookmarkId == "" {
|
||||
return fmt.Errorf("invalid empty bookmark id")
|
||||
}
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
|
||||
if tx.Exists(query, bm.BookmarkId) {
|
||||
return fmt.Errorf("bookmarkid already exists")
|
||||
}
|
||||
query = `INSERT INTO bookmark ( bookmarkid, createdts, cmdstr, alias, tags, description)
|
||||
VALUES (:bookmarkid,:createdts,:cmdstr,:alias,:tags,:description)`
|
||||
tx.NamedExec(query, bm.ToMap())
|
||||
for _, tag := range append(bm.Tags, "") {
|
||||
query = `SELECT COALESCE(max(orderidx), 0) FROM bookmark_order WHERE tag = ?`
|
||||
maxOrder := tx.GetInt(query, tag)
|
||||
query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)`
|
||||
tx.Exec(query, tag, bm.BookmarkId, maxOrder+1)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return txErr
|
||||
}
|
||||
|
||||
const (
|
||||
BookmarkField_Desc = "desc"
|
||||
BookmarkField_CmdStr = "cmdstr"
|
||||
)
|
||||
|
||||
func EditBookmark(ctx context.Context, bookmarkId string, editMap map[string]interface{}) error {
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
|
||||
if !tx.Exists(query, bookmarkId) {
|
||||
return fmt.Errorf("bookmark not found")
|
||||
}
|
||||
if desc, found := editMap[BookmarkField_Desc]; found {
|
||||
query = `UPDATE bookmark SET description = ? WHERE bookmarkid = ?`
|
||||
tx.Exec(query, desc, bookmarkId)
|
||||
}
|
||||
if cmdStr, found := editMap[BookmarkField_CmdStr]; found {
|
||||
query = `UPDATE bookmark SET cmdstr = ? WHERE bookmarkid = ?`
|
||||
tx.Exec(query, cmdStr, bookmarkId)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return txErr
|
||||
}
|
||||
|
||||
func fixupBookmarkOrder(tx *sstore.TxWrap) {
|
||||
query := `
|
||||
WITH new_order AS (
|
||||
SELECT tag, bookmarkid, row_number() OVER (PARTITION BY tag ORDER BY orderidx) AS newidx FROM bookmark_order
|
||||
)
|
||||
UPDATE bookmark_order
|
||||
SET orderidx = new_order.newidx
|
||||
FROM new_order
|
||||
WHERE bookmark_order.tag = new_order.tag AND bookmark_order.bookmarkid = new_order.bookmarkid
|
||||
`
|
||||
tx.Exec(query)
|
||||
}
|
||||
|
||||
func DeleteBookmark(ctx context.Context, bookmarkId string) error {
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
|
||||
if !tx.Exists(query, bookmarkId) {
|
||||
return fmt.Errorf("bookmark not found")
|
||||
}
|
||||
query = `DELETE FROM bookmark WHERE bookmarkid = ?`
|
||||
tx.Exec(query, bookmarkId)
|
||||
query = `DELETE FROM bookmark_order WHERE bookmarkid = ?`
|
||||
tx.Exec(query, bookmarkId)
|
||||
fixupBookmarkOrder(tx)
|
||||
return nil
|
||||
})
|
||||
return txErr
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bookmarks
|
||||
|
||||
import "github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
|
||||
type BookmarksUpdate struct {
|
||||
Bookmarks []*BookmarkType `json:"bookmarks"`
|
||||
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
||||
}
|
||||
|
||||
func (BookmarksUpdate) GetType() string {
|
||||
return "bookmarks"
|
||||
}
|
||||
|
||||
func AddBookmarksUpdate(update *scbus.ModelUpdatePacketType, bookmarks []*BookmarkType, selectedBookmark *string) {
|
||||
if selectedBookmark == nil {
|
||||
update.AddUpdate(BookmarksUpdate{Bookmarks: bookmarks})
|
||||
} else {
|
||||
update.AddUpdate(BookmarksUpdate{Bookmarks: bookmarks, SelectedBookmark: *selectedBookmark})
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,10 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/bookmarks"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/history"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
|
||||
@@ -47,6 +49,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
@@ -473,7 +476,7 @@ func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string,
|
||||
foundHistoryNum := hnum
|
||||
if hnum == -1 {
|
||||
var err error
|
||||
foundHistoryNum, err = sstore.GetLastHistoryLineNum(ctx, ids.ScreenId)
|
||||
foundHistoryNum, err = history.GetLastHistoryLineNum(ctx, ids.ScreenId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot expand history, error finding last history item: %v", err)
|
||||
}
|
||||
@@ -481,7 +484,7 @@ func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string,
|
||||
return "", fmt.Errorf("cannot expand history, no last history item")
|
||||
}
|
||||
}
|
||||
hitem, err := sstore.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum)
|
||||
hitem, err := history.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot get history item '%d': %v", foundHistoryNum, err)
|
||||
}
|
||||
@@ -666,7 +669,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hitem := &sstore.HistoryItemType{
|
||||
hitem := &history.HistoryItemType{
|
||||
HistoryId: scbase.GenWaveUUID(),
|
||||
Ts: time.Now().UnixMilli(),
|
||||
UserId: DefaultUserId,
|
||||
@@ -690,7 +693,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
|
||||
if !isMetaCmd && historyContext.RemotePtr != nil {
|
||||
hitem.Remote = *historyContext.RemotePtr
|
||||
}
|
||||
err = sstore.InsertHistoryItem(ctx, hitem)
|
||||
err = history.InsertHistoryItem(ctx, hitem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -706,7 +709,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
|
||||
}
|
||||
evalDepth := getEvalDepth(ctx)
|
||||
if pk.Interactive && evalDepth == 0 {
|
||||
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{NumCommands: 1}, "numcommands")
|
||||
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{NumCommands: 1}, "numcommands")
|
||||
}
|
||||
if evalDepth > MaxEvalDepth {
|
||||
return nil, fmt.Errorf("alias/history expansion max-depth exceeded")
|
||||
@@ -3352,8 +3355,8 @@ func validateRemoteColor(color string, typeStr string) error {
|
||||
}
|
||||
|
||||
func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
activity := sstore.ActivityUpdate{ClickShared: 1}
|
||||
sstore.UpdateActivityWrap(ctx, activity, "click-shared")
|
||||
activity := telemetry.ActivityUpdate{ClickShared: 1}
|
||||
telemetry.UpdateActivityWrap(ctx, activity, "click-shared")
|
||||
return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)")
|
||||
}
|
||||
|
||||
@@ -3615,11 +3618,11 @@ func MainViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
|
||||
update := scbus.MakeUpdatePacket()
|
||||
mainViewArg := pk.Args[0]
|
||||
if mainViewArg == sstore.MainViewSession {
|
||||
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSession})
|
||||
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewSession})
|
||||
} else if mainViewArg == sstore.MainViewConnections {
|
||||
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewConnections})
|
||||
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewConnections})
|
||||
} else if mainViewArg == sstore.MainViewSettings {
|
||||
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSettings})
|
||||
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewSettings})
|
||||
} else if mainViewArg == sstore.MainViewHistory {
|
||||
return nil, fmt.Errorf("use /history instead")
|
||||
} else if mainViewArg == sstore.MainViewBookmarks {
|
||||
@@ -3825,7 +3828,7 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
}
|
||||
historyIds = append(historyIds, historyArg)
|
||||
}
|
||||
err := sstore.PurgeHistoryByIds(ctx, historyIds)
|
||||
err := history.PurgeHistoryByIds(ctx, historyIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/history:purge error purging items: %v", err)
|
||||
}
|
||||
@@ -3837,7 +3840,7 @@ const HistoryViewPageSize = 50
|
||||
var cmdFilterLs = regexp.MustCompile(`^ls(\s|$)`)
|
||||
var cmdFilterCd = regexp.MustCompile(`^cd(\s|$)`)
|
||||
|
||||
func historyCmdFilter(hitem *sstore.HistoryItemType) bool {
|
||||
func historyCmdFilter(hitem *history.HistoryItemType) bool {
|
||||
cmdStr := hitem.CmdStr
|
||||
if cmdStr == "" || strings.Index(cmdStr, ";") != -1 || strings.Index(cmdStr, "\n") != -1 {
|
||||
return true
|
||||
@@ -3864,7 +3867,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset}
|
||||
opts := history.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset}
|
||||
if pk.Kwargs["text"] != "" {
|
||||
opts.SearchText = pk.Kwargs["text"]
|
||||
}
|
||||
@@ -3903,25 +3906,25 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err)
|
||||
}
|
||||
hresult, err := sstore.GetHistoryItems(ctx, opts)
|
||||
hresult, err := history.GetHistoryItems(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hvdata := &sstore.HistoryViewData{
|
||||
hvdata := &history.HistoryViewData{
|
||||
Items: hresult.Items,
|
||||
Offset: hresult.Offset,
|
||||
RawOffset: hresult.RawOffset,
|
||||
NextRawOffset: hresult.NextRawOffset,
|
||||
HasMore: hresult.HasMore,
|
||||
}
|
||||
lines, cmds, err := sstore.GetLineCmdsFromHistoryItems(ctx, hvdata.Items)
|
||||
lines, cmds, err := history.GetLineCmdsFromHistoryItems(ctx, hvdata.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hvdata.Lines = lines
|
||||
hvdata.Cmds = cmds
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewHistory, HistoryView: hvdata})
|
||||
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewHistory, HistoryView: hvdata})
|
||||
return update, nil
|
||||
}
|
||||
|
||||
@@ -3957,17 +3960,17 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
|
||||
} else if htype == HistoryTypeSession {
|
||||
hScreenId = ""
|
||||
}
|
||||
hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId}
|
||||
hresult, err := sstore.GetHistoryItems(ctx, hopts)
|
||||
hopts := history.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId}
|
||||
hresult, err := history.GetHistoryItems(ctx, hopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
show := !resolveBool(pk.Kwargs["noshow"], false)
|
||||
if show {
|
||||
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{HistoryView: 1}, "history")
|
||||
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{HistoryView: 1}, "history")
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.HistoryInfoType{
|
||||
update.AddUpdate(history.HistoryInfoType{
|
||||
HistoryType: htype,
|
||||
SessionId: ids.SessionId,
|
||||
ScreenId: ids.ScreenId,
|
||||
@@ -4310,16 +4313,16 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
if len(pk.Args) > 0 {
|
||||
tagName = pk.Args[0]
|
||||
}
|
||||
bms, err := sstore.GetBookmarks(ctx, tagName)
|
||||
bms, err := bookmarks.GetBookmarks(ctx, tagName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err)
|
||||
}
|
||||
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{BookmarksView: 1}, "bookmarks")
|
||||
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{BookmarksView: 1}, "bookmarks")
|
||||
update := scbus.MakeUpdatePacket()
|
||||
|
||||
update.AddUpdate(&sstore.MainViewUpdate{
|
||||
update.AddUpdate(&MainViewUpdate{
|
||||
MainView: sstore.MainViewBookmarks,
|
||||
BookmarksView: &sstore.BookmarksUpdate{Bookmarks: bms},
|
||||
BookmarksView: &bookmarks.BookmarksUpdate{Bookmarks: bms},
|
||||
})
|
||||
return update, nil
|
||||
}
|
||||
@@ -4329,7 +4332,7 @@ func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
||||
return nil, fmt.Errorf("/bookmark:set requires one argument (bookmark id)")
|
||||
}
|
||||
bookmarkArg := pk.Args[0]
|
||||
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
|
||||
bookmarkId, err := bookmarks.GetBookmarkIdByArg(ctx, bookmarkArg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
|
||||
}
|
||||
@@ -4338,25 +4341,25 @@ func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
||||
}
|
||||
editMap := make(map[string]interface{})
|
||||
if descStr, found := pk.Kwargs["desc"]; found {
|
||||
editMap[sstore.BookmarkField_Desc] = descStr
|
||||
editMap[bookmarks.BookmarkField_Desc] = descStr
|
||||
}
|
||||
if cmdStr, found := pk.Kwargs["cmdstr"]; found {
|
||||
editMap[sstore.BookmarkField_CmdStr] = cmdStr
|
||||
editMap[bookmarks.BookmarkField_CmdStr] = cmdStr
|
||||
}
|
||||
if len(editMap) == 0 {
|
||||
return nil, fmt.Errorf("no fields set, can set %s", formatStrs([]string{"desc", "cmdstr"}, "or", false))
|
||||
}
|
||||
err = sstore.EditBookmark(ctx, bookmarkId, editMap)
|
||||
err = bookmarks.EditBookmark(ctx, bookmarkId, editMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error trying to edit bookmark: %v", err)
|
||||
}
|
||||
bm, err := sstore.GetBookmarkById(ctx, bookmarkId, "")
|
||||
bm, err := bookmarks.GetBookmarkById(ctx, bookmarkId, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving edited bookmark: %v", err)
|
||||
}
|
||||
bms := []*sstore.BookmarkType{bm}
|
||||
bms := []*bookmarks.BookmarkType{bm}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
sstore.AddBookmarksUpdate(update, bms, nil)
|
||||
bookmarks.AddBookmarksUpdate(update, bms, nil)
|
||||
update.AddUpdate(sstore.InfoMsgUpdate("bookmark edited"))
|
||||
return update, nil
|
||||
}
|
||||
@@ -4366,20 +4369,20 @@ func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
|
||||
return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)")
|
||||
}
|
||||
bookmarkArg := pk.Args[0]
|
||||
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
|
||||
bookmarkId, err := bookmarks.GetBookmarkIdByArg(ctx, bookmarkArg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
|
||||
}
|
||||
if bookmarkId == "" {
|
||||
return nil, fmt.Errorf("bookmark not found")
|
||||
}
|
||||
err = sstore.DeleteBookmark(ctx, bookmarkId)
|
||||
err = bookmarks.DeleteBookmark(ctx, bookmarkId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deleting bookmark: %v", err)
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
bms := []*sstore.BookmarkType{{BookmarkId: bookmarkId, Remove: true}}
|
||||
sstore.AddBookmarksUpdate(update, bms, nil)
|
||||
bms := []*bookmarks.BookmarkType{{BookmarkId: bookmarkId, Remove: true}}
|
||||
bookmarks.AddBookmarksUpdate(update, bms, nil)
|
||||
update.AddUpdate(sstore.InfoMsgUpdate("bookmark deleted"))
|
||||
return update, nil
|
||||
}
|
||||
@@ -4407,7 +4410,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
if cmdObj == nil {
|
||||
return nil, fmt.Errorf("cannot bookmark non-cmd line")
|
||||
}
|
||||
existingBmIds, err := sstore.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr)
|
||||
existingBmIds, err := bookmarks.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error trying to retrieve current boookmarks: %v", err)
|
||||
}
|
||||
@@ -4415,7 +4418,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
if len(existingBmIds) > 0 {
|
||||
newBmId = existingBmIds[0]
|
||||
} else {
|
||||
newBm := &sstore.BookmarkType{
|
||||
newBm := &bookmarks.BookmarkType{
|
||||
BookmarkId: uuid.New().String(),
|
||||
CreatedTs: time.Now().UnixMilli(),
|
||||
CmdStr: cmdObj.CmdStr,
|
||||
@@ -4423,17 +4426,17 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
Tags: nil,
|
||||
Description: "",
|
||||
}
|
||||
err = sstore.InsertBookmark(ctx, newBm)
|
||||
err = bookmarks.InsertBookmark(ctx, newBm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot insert bookmark: %v", err)
|
||||
}
|
||||
newBmId = newBm.BookmarkId
|
||||
}
|
||||
bms, err := sstore.GetBookmarks(ctx, "")
|
||||
bms, err := bookmarks.GetBookmarks(ctx, "")
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(&sstore.MainViewUpdate{
|
||||
update.AddUpdate(&MainViewUpdate{
|
||||
MainView: sstore.MainViewBookmarks,
|
||||
BookmarksView: &sstore.BookmarksUpdate{Bookmarks: bms, SelectedBookmark: newBmId},
|
||||
BookmarksView: &bookmarks.BookmarksUpdate{Bookmarks: bms, SelectedBookmark: newBmId},
|
||||
})
|
||||
return update, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdrunner
|
||||
|
||||
import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/bookmarks"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/history"
|
||||
)
|
||||
|
||||
type MainViewUpdate struct {
|
||||
MainView string `json:"mainview"`
|
||||
HistoryView *history.HistoryViewData `json:"historyview,omitempty"`
|
||||
BookmarksView *bookmarks.BookmarksUpdate `json:"bookmarksview,omitempty"`
|
||||
}
|
||||
|
||||
func (MainViewUpdate) GetType() string {
|
||||
return "mainview"
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/workspaces/line"
|
||||
)
|
||||
|
||||
type HistoryItemType struct {
|
||||
HistoryId string `json:"historyid"`
|
||||
Ts int64 `json:"ts"`
|
||||
UserId string `json:"userid"`
|
||||
SessionId string `json:"sessionid"`
|
||||
ScreenId string `json:"screenid"`
|
||||
LineId string `json:"lineid"`
|
||||
HadError bool `json:"haderror"`
|
||||
CmdStr string `json:"cmdstr"`
|
||||
Remote sstore.RemotePtrType `json:"remote"`
|
||||
IsMetaCmd bool `json:"ismetacmd"`
|
||||
ExitCode *int64 `json:"exitcode,omitempty"`
|
||||
DurationMs *int64 `json:"durationms,omitempty"`
|
||||
FeState sstore.FeStateType `json:"festate,omitempty"`
|
||||
Tags map[string]bool `json:"tags,omitempty"`
|
||||
LineNum int64 `json:"linenum" dbmap:"-"`
|
||||
Status string `json:"status"`
|
||||
|
||||
// only for updates
|
||||
Remove bool `json:"remove" dbmap:"-"`
|
||||
|
||||
// transient (string because of different history orderings)
|
||||
HistoryNum string `json:"historynum" dbmap:"-"`
|
||||
}
|
||||
|
||||
func (h *HistoryItemType) ToMap() map[string]interface{} {
|
||||
rtn := make(map[string]interface{})
|
||||
rtn["historyid"] = h.HistoryId
|
||||
rtn["ts"] = h.Ts
|
||||
rtn["userid"] = h.UserId
|
||||
rtn["sessionid"] = h.SessionId
|
||||
rtn["screenid"] = h.ScreenId
|
||||
rtn["lineid"] = h.LineId
|
||||
rtn["linenum"] = h.LineNum
|
||||
rtn["haderror"] = h.HadError
|
||||
rtn["cmdstr"] = h.CmdStr
|
||||
rtn["remoteownerid"] = h.Remote.OwnerId
|
||||
rtn["remoteid"] = h.Remote.RemoteId
|
||||
rtn["remotename"] = h.Remote.Name
|
||||
rtn["ismetacmd"] = h.IsMetaCmd
|
||||
rtn["exitcode"] = h.ExitCode
|
||||
rtn["durationms"] = h.DurationMs
|
||||
rtn["festate"] = dbutil.QuickJson(h.FeState)
|
||||
rtn["tags"] = dbutil.QuickJson(h.Tags)
|
||||
rtn["status"] = h.Status
|
||||
return rtn
|
||||
}
|
||||
|
||||
func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
|
||||
dbutil.QuickSetStr(&h.HistoryId, m, "historyid")
|
||||
dbutil.QuickSetInt64(&h.Ts, m, "ts")
|
||||
dbutil.QuickSetStr(&h.UserId, m, "userid")
|
||||
dbutil.QuickSetStr(&h.SessionId, m, "sessionid")
|
||||
dbutil.QuickSetStr(&h.ScreenId, m, "screenid")
|
||||
dbutil.QuickSetStr(&h.LineId, m, "lineid")
|
||||
dbutil.QuickSetBool(&h.HadError, m, "haderror")
|
||||
dbutil.QuickSetStr(&h.CmdStr, m, "cmdstr")
|
||||
dbutil.QuickSetStr(&h.Remote.OwnerId, m, "remoteownerid")
|
||||
dbutil.QuickSetStr(&h.Remote.RemoteId, m, "remoteid")
|
||||
dbutil.QuickSetStr(&h.Remote.Name, m, "remotename")
|
||||
dbutil.QuickSetBool(&h.IsMetaCmd, m, "ismetacmd")
|
||||
dbutil.QuickSetStr(&h.HistoryNum, m, "historynum")
|
||||
dbutil.QuickSetInt64(&h.LineNum, m, "linenum")
|
||||
dbutil.QuickSetNullableInt64(&h.ExitCode, m, "exitcode")
|
||||
dbutil.QuickSetNullableInt64(&h.DurationMs, m, "durationms")
|
||||
dbutil.QuickSetJson(&h.FeState, m, "festate")
|
||||
dbutil.QuickSetJson(&h.Tags, m, "tags")
|
||||
dbutil.QuickSetStr(&h.Status, m, "status")
|
||||
return true
|
||||
}
|
||||
|
||||
type HistoryQueryOpts struct {
|
||||
Offset int
|
||||
MaxItems int
|
||||
FromTs int64
|
||||
SearchText string
|
||||
SessionId string
|
||||
RemoteId string
|
||||
ScreenId string
|
||||
NoMeta bool
|
||||
RawOffset int
|
||||
FilterFn func(*HistoryItemType) bool
|
||||
}
|
||||
|
||||
type HistoryQueryResult struct {
|
||||
MaxItems int
|
||||
Items []*HistoryItemType
|
||||
Offset int // the offset shown to user
|
||||
RawOffset int // internal offset
|
||||
HasMore bool
|
||||
NextRawOffset int // internal offset used by pager for next query
|
||||
|
||||
prevItems int // holds number of items skipped by RawOffset
|
||||
}
|
||||
|
||||
type HistoryViewData struct {
|
||||
Items []*HistoryItemType `json:"items"`
|
||||
Offset int `json:"offset"`
|
||||
RawOffset int `json:"rawoffset"`
|
||||
NextRawOffset int `json:"nextrawoffset"`
|
||||
HasMore bool `json:"hasmore"`
|
||||
Lines []*line.LineType `json:"lines"`
|
||||
Cmds []*sstore.CmdType `json:"cmds"`
|
||||
}
|
||||
|
||||
const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.linenum, h.exitcode, h.durationms, h.festate, h.tags, h.status"
|
||||
const DefaultMaxHistoryItems = 1000
|
||||
|
||||
func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error {
|
||||
if hitem == nil {
|
||||
return fmt.Errorf("cannot insert nil history item")
|
||||
}
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `INSERT INTO history
|
||||
( historyid, ts, userid, sessionid, screenid, lineid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, linenum, exitcode, durationms, festate, tags, status) VALUES
|
||||
(:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:linenum,:exitcode,:durationms,:festate,:tags,:status)`
|
||||
tx.NamedExec(query, hitem.ToMap())
|
||||
return nil
|
||||
})
|
||||
return txErr
|
||||
}
|
||||
|
||||
const HistoryQueryChunkSize = 1000
|
||||
|
||||
func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*HistoryItemType) bool) (*HistoryItemType, int) {
|
||||
for ; index < len(items); index++ {
|
||||
item := items[index]
|
||||
if filterFn(item) {
|
||||
return item, index
|
||||
}
|
||||
}
|
||||
return nil, index
|
||||
}
|
||||
|
||||
// returns true if done, false if we still need to process more items
|
||||
func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset int) bool {
|
||||
if result.prevItems < result.Offset {
|
||||
result.prevItems++
|
||||
return false
|
||||
}
|
||||
if len(result.Items) == result.MaxItems {
|
||||
result.HasMore = true
|
||||
result.NextRawOffset = rawOffset
|
||||
return true
|
||||
}
|
||||
if len(result.Items) == 0 {
|
||||
result.RawOffset = rawOffset
|
||||
}
|
||||
result.Items = append(result.Items, item)
|
||||
return false
|
||||
}
|
||||
|
||||
func runHistoryQueryWithFilter(tx *sstore.TxWrap, opts HistoryQueryOpts) (*HistoryQueryResult, error) {
|
||||
if opts.MaxItems == 0 {
|
||||
return nil, fmt.Errorf("invalid query, maxitems is 0")
|
||||
}
|
||||
rtn := &HistoryQueryResult{Offset: opts.Offset, MaxItems: opts.MaxItems}
|
||||
var rawOffset int
|
||||
if opts.RawOffset >= opts.Offset {
|
||||
rtn.prevItems = opts.Offset
|
||||
rawOffset = opts.RawOffset
|
||||
} else {
|
||||
rawOffset = 0
|
||||
}
|
||||
for {
|
||||
resultItems, err := runHistoryQuery(tx, opts, rawOffset, HistoryQueryChunkSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isDone := false
|
||||
for resultIdx := 0; resultIdx < len(resultItems); resultIdx++ {
|
||||
if opts.FilterFn != nil && !opts.FilterFn(resultItems[resultIdx]) {
|
||||
continue
|
||||
}
|
||||
isDone = rtn.processItem(resultItems[resultIdx], rawOffset+resultIdx)
|
||||
if isDone {
|
||||
break
|
||||
}
|
||||
}
|
||||
if isDone {
|
||||
break
|
||||
}
|
||||
if len(resultItems) < HistoryQueryChunkSize {
|
||||
break
|
||||
}
|
||||
rawOffset += HistoryQueryChunkSize
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func runHistoryQuery(tx *sstore.TxWrap, opts HistoryQueryOpts, realOffset int, itemLimit int) ([]*HistoryItemType, error) {
|
||||
// check sessionid/screenid format because we are directly inserting them into the SQL
|
||||
if opts.SessionId != "" {
|
||||
_, err := uuid.Parse(opts.SessionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed sessionid")
|
||||
}
|
||||
}
|
||||
if opts.ScreenId != "" {
|
||||
_, err := uuid.Parse(opts.ScreenId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed screenid")
|
||||
}
|
||||
}
|
||||
if opts.RemoteId != "" {
|
||||
_, err := uuid.Parse(opts.RemoteId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed remoteid")
|
||||
}
|
||||
}
|
||||
whereClause := "WHERE 1"
|
||||
var queryArgs []interface{}
|
||||
hNumStr := ""
|
||||
if opts.SessionId != "" && opts.ScreenId != "" {
|
||||
whereClause += fmt.Sprintf(" AND h.sessionid = '%s' AND h.screenid = '%s'", opts.SessionId, opts.ScreenId)
|
||||
hNumStr = ""
|
||||
} else if opts.SessionId != "" {
|
||||
whereClause += fmt.Sprintf(" AND h.sessionid = '%s'", opts.SessionId)
|
||||
hNumStr = "s"
|
||||
} else {
|
||||
hNumStr = "g"
|
||||
}
|
||||
if opts.SearchText != "" {
|
||||
whereClause += " AND h.cmdstr LIKE ? ESCAPE '\\'"
|
||||
likeArg := opts.SearchText
|
||||
likeArg = strings.ReplaceAll(likeArg, "%", "\\%")
|
||||
likeArg = strings.ReplaceAll(likeArg, "_", "\\_")
|
||||
queryArgs = append(queryArgs, "%"+likeArg+"%")
|
||||
}
|
||||
if opts.FromTs > 0 {
|
||||
whereClause += fmt.Sprintf(" AND h.ts <= %d", opts.FromTs)
|
||||
}
|
||||
if opts.RemoteId != "" {
|
||||
whereClause += fmt.Sprintf(" AND h.remoteid = '%s'", opts.RemoteId)
|
||||
}
|
||||
if opts.NoMeta {
|
||||
whereClause += " AND NOT h.ismetacmd"
|
||||
}
|
||||
query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum FROM history h %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset)
|
||||
marr := tx.SelectMaps(query, queryArgs...)
|
||||
rtn := make([]*HistoryItemType, len(marr))
|
||||
for idx, m := range marr {
|
||||
hitem := dbutil.FromMap[*HistoryItemType](m)
|
||||
rtn[idx] = hitem
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryResult, error) {
|
||||
var rtn *HistoryQueryResult
|
||||
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
var err error
|
||||
rtn, err = runHistoryQueryWithFilter(tx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func GetHistoryItemByLineNum(ctx context.Context, screenId string, lineNum int) (*HistoryItemType, error) {
|
||||
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*HistoryItemType, error) {
|
||||
query := `SELECT * FROM history WHERE screenid = ? AND linenum = ?`
|
||||
hitem := dbutil.GetMapGen[*HistoryItemType](tx, query, screenId, lineNum)
|
||||
return hitem, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetLastHistoryLineNum(ctx context.Context, screenId string) (int, error) {
|
||||
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (int, error) {
|
||||
query := `SELECT COALESCE(max(linenum), 0) FROM history WHERE screenid = ?`
|
||||
maxLineNum := tx.GetInt(query, screenId)
|
||||
return maxLineNum, nil
|
||||
})
|
||||
}
|
||||
|
||||
func getLineIdsFromHistoryItems(historyItems []*HistoryItemType) []string {
|
||||
var rtn []string
|
||||
for _, hitem := range historyItems {
|
||||
if hitem.LineId != "" {
|
||||
rtn = append(rtn, hitem.LineId)
|
||||
}
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryItemType) ([]*line.LineType, []*sstore.CmdType, error) {
|
||||
if len(historyItems) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return sstore.WithTxRtn3(ctx, func(tx *sstore.TxWrap) ([]*line.LineType, []*sstore.CmdType, error) {
|
||||
lineIdsJsonArr := dbutil.QuickJsonArr(getLineIdsFromHistoryItems(historyItems))
|
||||
query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))`
|
||||
lineArr := dbutil.SelectMappable[*line.LineType](tx, query, lineIdsJsonArr)
|
||||
query = `SELECT * FROM cmd WHERE lineid IN (SELECT value FROM json_each(?))`
|
||||
cmdArr := dbutil.SelectMapsGen[*sstore.CmdType](tx, query, lineIdsJsonArr)
|
||||
return lineArr, cmdArr, nil
|
||||
})
|
||||
}
|
||||
|
||||
func PurgeHistoryByIds(ctx context.Context, historyIds []string) error {
|
||||
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
query := `DELETE FROM history WHERE historyid IN (SELECT value FROM json_each(?))`
|
||||
tx.Exec(query, dbutil.QuickJsonArr(historyIds))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package history
|
||||
|
||||
type HistoryInfoType struct {
|
||||
HistoryType string `json:"historytype"`
|
||||
SessionId string `json:"sessionid,omitempty"`
|
||||
ScreenId string `json:"screenid,omitempty"`
|
||||
Items []*HistoryItemType `json:"items"`
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
|
||||
func (HistoryInfoType) GetType() string {
|
||||
return "history"
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
|
||||
)
|
||||
|
||||
const PCloudEndpoint = "https://api.waveterm.dev/central"
|
||||
@@ -156,7 +157,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
|
||||
if !force && clientData.ClientOpts.NoTelemetry {
|
||||
return nil
|
||||
}
|
||||
activity, err := sstore.GetNonUploadedActivity(ctx)
|
||||
activity, err := telemetry.GetNonUploadedActivity(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get activity: %v", err)
|
||||
}
|
||||
@@ -164,7 +165,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
|
||||
return nil
|
||||
}
|
||||
log.Printf("[pcloud] sending telemetry data\n")
|
||||
dayStr := sstore.GetCurDayStr()
|
||||
dayStr := telemetry.GetCurDayStr()
|
||||
defaultShellType := shellapi.DetectLocalShellType()
|
||||
input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, CurDay: dayStr, DefaultShell: defaultShellType, Activity: activity}
|
||||
req, err := makeAnonPostReq(ctx, TelemetryUrl, input)
|
||||
@@ -175,7 +176,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = sstore.MarkActivityAsUploaded(ctx, activity)
|
||||
err = telemetry.MarkActivityAsUploaded(ctx, activity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marking activity as uploaded: %v", err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
|
||||
)
|
||||
|
||||
type NoTelemetryInputType struct {
|
||||
@@ -19,11 +20,11 @@ type NoTelemetryInputType struct {
|
||||
}
|
||||
|
||||
type TelemetryInputType struct {
|
||||
UserId string `json:"userid"`
|
||||
ClientId string `json:"clientid"`
|
||||
CurDay string `json:"curday"`
|
||||
DefaultShell string `json:"defaultshell"`
|
||||
Activity []*sstore.ActivityType `json:"activity"`
|
||||
UserId string `json:"userid"`
|
||||
ClientId string `json:"clientid"`
|
||||
CurDay string `json:"curday"`
|
||||
DefaultShell string `json:"defaultshell"`
|
||||
Activity []*telemetry.ActivityType `json:"activity"`
|
||||
}
|
||||
|
||||
type WebShareUpdateType struct {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package playbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
)
|
||||
|
||||
type PlaybookType struct {
|
||||
PlaybookId string `json:"playbookid"`
|
||||
PlaybookName string `json:"playbookname"`
|
||||
Description string `json:"description"`
|
||||
EntryIds []string `json:"entryids"`
|
||||
|
||||
// this is not persisted to DB, just for transport to FE
|
||||
Entries []*PlaybookEntry `json:"entries"`
|
||||
}
|
||||
|
||||
func (p *PlaybookType) ToMap() map[string]interface{} {
|
||||
rtn := make(map[string]interface{})
|
||||
rtn["playbookid"] = p.PlaybookId
|
||||
rtn["playbookname"] = p.PlaybookName
|
||||
rtn["description"] = p.Description
|
||||
rtn["entryids"] = dbutil.QuickJsonArr(p.EntryIds)
|
||||
return rtn
|
||||
}
|
||||
|
||||
func (p *PlaybookType) FromMap(m map[string]interface{}) bool {
|
||||
dbutil.QuickSetStr(&p.PlaybookId, m, "playbookid")
|
||||
dbutil.QuickSetStr(&p.PlaybookName, m, "playbookname")
|
||||
dbutil.QuickSetStr(&p.Description, m, "description")
|
||||
dbutil.QuickSetJsonArr(&p.Entries, m, "entries")
|
||||
return true
|
||||
}
|
||||
|
||||
// reorders p.Entries to match p.EntryIds
|
||||
func (p *PlaybookType) OrderEntries() {
|
||||
if len(p.Entries) == 0 {
|
||||
return
|
||||
}
|
||||
m := make(map[string]*PlaybookEntry)
|
||||
for _, entry := range p.Entries {
|
||||
m[entry.EntryId] = entry
|
||||
}
|
||||
newList := make([]*PlaybookEntry, 0, len(p.EntryIds))
|
||||
for _, entryId := range p.EntryIds {
|
||||
entry := m[entryId]
|
||||
if entry != nil {
|
||||
newList = append(newList, entry)
|
||||
}
|
||||
}
|
||||
p.Entries = newList
|
||||
}
|
||||
|
||||
// removes from p.EntryIds (not from p.Entries)
|
||||
func (p *PlaybookType) RemoveEntry(entryIdToRemove string) {
|
||||
if len(p.EntryIds) == 0 {
|
||||
return
|
||||
}
|
||||
newList := make([]string, 0, len(p.EntryIds)-1)
|
||||
for _, entryId := range p.EntryIds {
|
||||
if entryId == entryIdToRemove {
|
||||
continue
|
||||
}
|
||||
newList = append(newList, entryId)
|
||||
}
|
||||
p.EntryIds = newList
|
||||
}
|
||||
|
||||
type PlaybookEntry struct {
|
||||
PlaybookId string `json:"playbookid"`
|
||||
EntryId string `json:"entryid"`
|
||||
Alias string `json:"alias"`
|
||||
CmdStr string `json:"cmdstr"`
|
||||
UpdatedTs int64 `json:"updatedts"`
|
||||
CreatedTs int64 `json:"createdts"`
|
||||
Description string `json:"description"`
|
||||
Remove bool `json:"remove,omitempty"`
|
||||
}
|
||||
|
||||
func CreatePlaybook(ctx context.Context, name string) (*PlaybookType, error) {
|
||||
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*PlaybookType, error) {
|
||||
query := `SELECT playbookid FROM playbook WHERE name = ?`
|
||||
if tx.Exists(query, name) {
|
||||
return nil, fmt.Errorf("playbook %q already exists", name)
|
||||
}
|
||||
rtn := &PlaybookType{}
|
||||
rtn.PlaybookId = uuid.New().String()
|
||||
rtn.PlaybookName = name
|
||||
query = `INSERT INTO playbook ( playbookid, playbookname, description, entryids)
|
||||
VALUES (:playbookid,:playbookname,:description,:entryids)`
|
||||
tx.Exec(query, rtn.ToMap())
|
||||
return rtn, nil
|
||||
})
|
||||
}
|
||||
|
||||
func selectPlaybook(tx *sstore.TxWrap, playbookId string) *PlaybookType {
|
||||
query := `SELECT * FROM playbook where playbookid = ?`
|
||||
playbook := dbutil.GetMapGen[*PlaybookType](tx, query, playbookId)
|
||||
return playbook
|
||||
}
|
||||
|
||||
func AddPlaybookEntry(ctx context.Context, entry *PlaybookEntry) error {
|
||||
if entry.EntryId == "" {
|
||||
return fmt.Errorf("invalid entryid")
|
||||
}
|
||||
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
playbook := selectPlaybook(tx, entry.PlaybookId)
|
||||
if playbook == nil {
|
||||
return fmt.Errorf("cannot add entry, playbook does not exist")
|
||||
}
|
||||
query := `SELECT entryid FROM playbook_entry WHERE entryid = ?`
|
||||
if tx.Exists(query, entry.EntryId) {
|
||||
return fmt.Errorf("cannot add entry, entryid already exists")
|
||||
}
|
||||
query = `INSERT INTO playbook_entry ( entryid, playbookid, description, alias, cmdstr, createdts, updatedts)
|
||||
VALUES (:entryid,:playbookid,:description,:alias,:cmdstr,:createdts,:updatedts)`
|
||||
tx.Exec(query, entry)
|
||||
playbook.EntryIds = append(playbook.EntryIds, entry.EntryId)
|
||||
query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?`
|
||||
tx.Exec(query, dbutil.QuickJsonArr(playbook.EntryIds), entry.PlaybookId)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func RemovePlaybookEntry(ctx context.Context, playbookId string, entryId string) error {
|
||||
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
|
||||
playbook := selectPlaybook(tx, playbookId)
|
||||
if playbook == nil {
|
||||
return fmt.Errorf("cannot remove playbook entry, playbook does not exist")
|
||||
}
|
||||
query := `SELECT entryid FROM playbook_entry WHERE entryid = ?`
|
||||
if !tx.Exists(query, entryId) {
|
||||
return fmt.Errorf("cannot remove playbook entry, entry does not exist")
|
||||
}
|
||||
query = `DELETE FROM playbook_entry WHERE entryid = ?`
|
||||
tx.Exec(query, entryId)
|
||||
playbook.RemoveEntry(entryId)
|
||||
query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?`
|
||||
tx.Exec(query, dbutil.QuickJsonArr(playbook.EntryIds), playbookId)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetPlaybookById(ctx context.Context, playbookId string) (*PlaybookType, error) {
|
||||
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*PlaybookType, error) {
|
||||
rtn := selectPlaybook(tx, playbookId)
|
||||
if rtn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
query := `SELECT * FROM playbook_entry WHERE playbookid = ?`
|
||||
tx.Select(&rtn.Entries, query, playbookId)
|
||||
rtn.OrderEntries()
|
||||
return rtn, nil
|
||||
})
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -1419,8 +1420,8 @@ func getStateVarsFromInitPk(initPk *packet.InitPacketType) map[string]string {
|
||||
return rtn
|
||||
}
|
||||
|
||||
func makeReinitErrorUpdate(shellType string) sstore.ActivityUpdate {
|
||||
rtn := sstore.ActivityUpdate{}
|
||||
func makeReinitErrorUpdate(shellType string) telemetry.ActivityUpdate {
|
||||
rtn := telemetry.ActivityUpdate{}
|
||||
if shellType == packet.ShellType_bash {
|
||||
rtn.ReinitBashErrors = 1
|
||||
} else if shellType == packet.ShellType_zsh {
|
||||
@@ -1441,7 +1442,7 @@ func (msh *MShellProc) ReInit(ctx context.Context, ck base.CommandKey, shellType
|
||||
}
|
||||
defer func() {
|
||||
if rtnErr != nil {
|
||||
sstore.UpdateActivityWrap(ctx, makeReinitErrorUpdate(shellType), "reiniterror")
|
||||
telemetry.UpdateActivityWrap(ctx, makeReinitErrorUpdate(shellType), "reiniterror")
|
||||
}
|
||||
}()
|
||||
startTs := time.Now()
|
||||
|
||||
+99
-1292
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,8 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
@@ -126,69 +124,6 @@ func ReadPtyOutFile(ctx context.Context, screenId string, lineId string, offset
|
||||
return f.ReadAtWithMax(ctx, offset, maxSize)
|
||||
}
|
||||
|
||||
type SessionDiskSizeType struct {
|
||||
NumFiles int
|
||||
TotalSize int64
|
||||
ErrorCount int
|
||||
Location string
|
||||
}
|
||||
|
||||
func directorySize(dirName string) (SessionDiskSizeType, error) {
|
||||
var rtn SessionDiskSizeType
|
||||
rtn.Location = dirName
|
||||
entries, err := os.ReadDir(dirName)
|
||||
if err != nil {
|
||||
return rtn, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
rtn.ErrorCount++
|
||||
continue
|
||||
}
|
||||
finfo, err := entry.Info()
|
||||
if err != nil {
|
||||
rtn.ErrorCount++
|
||||
continue
|
||||
}
|
||||
rtn.NumFiles++
|
||||
rtn.TotalSize += finfo.Size()
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func SessionDiskSize(sessionId string) (SessionDiskSizeType, error) {
|
||||
sessionDir, err := scbase.EnsureSessionDir(sessionId)
|
||||
if err != nil {
|
||||
return SessionDiskSizeType{}, err
|
||||
}
|
||||
return directorySize(sessionDir)
|
||||
}
|
||||
|
||||
func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) {
|
||||
sdir := scbase.GetSessionsDir()
|
||||
entries, err := os.ReadDir(sdir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rtn := make(map[string]SessionDiskSizeType)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
_, err = uuid.Parse(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
diskSize, err := directorySize(path.Join(sdir, name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rtn[name] = diskSize
|
||||
}
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func DeletePtyOutFile(ctx context.Context, screenId string, lineId string) error {
|
||||
ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId)
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user