Embed static copy of docsite for help view (#949)

This will take the latest artifact from the waveterm-docs repo and embed
it in the app binary. When the help view is launched, it will be served
from our backend. If the embedded copy doesn't exist, such as in
unpackaged versions of the app or in locally packaged versions, it will
use the hosted site instead.

There is a sibling PR in the docs repository to build the embedded
version of the app (strips out some external links, removes Algolia
DocSearch, updates the baseUrl)
https://github.com/wavetermdev/waveterm-docs/pull/46
This commit is contained in:
Evan Simkowitz 2024-10-03 20:28:05 -07:00 committed by GitHub
parent cee46a6db9
commit 74cda378f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 120 additions and 219 deletions

View File

@ -11,6 +11,7 @@ on:
env:
GO_VERSION: "1.22"
NODE_VERSION: "20"
STATIC_DOCSITE_PATH: docsite
jobs:
runbuild:
permissions:
@ -110,6 +111,15 @@ jobs:
smctl windows certsync
shell: cmd
- name: Download waveterm-docs static site
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{secrets.GITHUB_TOKEN}}
workflow: build-embedded.yml
repo: wavetermdev/waveterm-docs
name: static-site
path: ${{env.STATIC_DOCSITE_PATH}}
# Build and upload packages
- name: Build (not Windows)
if: matrix.platform != 'windows'
@ -121,6 +131,7 @@ jobs:
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID_2 }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_PWD_2 }}
APPLE_TEAM_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_TEAM_ID_2 }}
STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}}
- name: Build (Windows only)
if: matrix.platform == 'windows'
run: task package
@ -128,6 +139,7 @@ jobs:
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
CSC_LINK: ${{ steps.variables.outputs.SM_CLIENT_CERT_FILE }}
CSC_KEY_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}}
shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
- name: Upload to S3 staging
run: task artifacts:upload

View File

@ -34,6 +34,7 @@ const config = {
},
asarUnpack: [
"dist/bin/**/*", // wavesrv and wsh binaries
"dist/docsite/**/*", // the static docsite
],
mac: {
target: [
@ -86,6 +87,14 @@ const config = {
provider: "generic",
url: "https://dl.waveterm.dev/releases-w2",
},
beforePack: () => {
const staticSourcePath = process.env.STATIC_DOCSITE_PATH;
const staticDestPath = "dist/docsite";
if (staticSourcePath) {
console.log(`Static docsite path is specified, copying from "${staticSourcePath}" to "${staticDestPath}"`);
fs.cpSync(staticSourcePath, staticDestPath, { recursive: true });
}
},
afterPack: (context) => {
// This is a workaround to restore file permissions to the wavesrv binaries on macOS after packaging the universal binary.
if (context.electronPlatformName === "darwin" && context.arch === Arch.universal) {

27
emain/docsite.ts Normal file
View File

@ -0,0 +1,27 @@
import { getWebServerEndpoint } from "@/util/endpoints";
import { fetch } from "@/util/fetchutil";
import { ipcMain } from "electron";
const docsiteWebUrl = "https://docs.waveterm.dev/";
let docsiteUrl: string;
ipcMain.on("get-docsite-url", (event) => {
event.returnValue = docsiteUrl;
});
export async function initDocsite() {
const docsiteEmbeddedUrl = getWebServerEndpoint() + "/docsite/";
try {
const response = await fetch(docsiteEmbeddedUrl);
if (response.ok) {
console.log("Embedded docsite is running, using embedded version for help view");
docsiteUrl = docsiteEmbeddedUrl;
} else {
console.log("Embedded docsite is not running, using web version for help view", response);
docsiteUrl = docsiteWebUrl;
}
} catch (error) {
console.log("Failed to fetch docsite url, using web version for help view", error);
docsiteUrl = docsiteWebUrl;
}
}

View File

@ -20,12 +20,13 @@ import { fetch } from "../frontend/util/fetchutil";
import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget } from "../frontend/util/util";
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
import { initDocsite } from "./docsite";
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
import { getLaunchSettings } from "./launchsettings";
import { getAppMenu } from "./menu";
import {
getElectronAppBasePath,
getGoAppBasePath,
getElectronAppUnpackedBasePath,
getWaveHomeDir,
getWaveSrvCwd,
getWaveSrvPath,
@ -95,7 +96,7 @@ console.log(
"waveterm-app starting, WAVETERM_HOME=%s, electronpath=%s gopath=%s arch=%s/%s",
waveHome,
getElectronAppBasePath(),
getGoAppBasePath(),
getElectronAppUnpackedBasePath(),
unamePlatform,
unameArch
)
@ -155,7 +156,7 @@ function runWaveSrv(): Promise<boolean> {
pReject = argReject;
});
const envCopy = { ...process.env };
envCopy[WaveAppPathVarName] = getGoAppBasePath();
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
envCopy[AuthKeyEnv] = AuthKey;
const waveSrvCmd = getWaveSrvPath();
@ -871,6 +872,7 @@ async function appMain() {
await electronApp.whenReady();
configureAuthKeyRequestInjection(electron.session.defaultSession);
await relaunchBrowserWindows();
await initDocsite();
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
try {
initElectronWshClient();

View File

@ -50,7 +50,7 @@ function getElectronAppBasePath(): string {
return path.dirname(import.meta.dirname);
}
function getGoAppBasePath(): string {
function getElectronAppUnpackedBasePath(): string {
return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked");
}
@ -59,10 +59,10 @@ const wavesrvBinName = `wavesrv.${unameArch}`;
function getWaveSrvPath(): string {
if (process.platform === "win32") {
const winBinName = `${wavesrvBinName}.exe`;
const appPath = path.join(getGoAppBasePath(), "bin", winBinName);
const appPath = path.join(getElectronAppUnpackedBasePath(), "bin", winBinName);
return `${appPath}`;
}
return path.join(getGoAppBasePath(), "bin", wavesrvBinName);
return path.join(getElectronAppUnpackedBasePath(), "bin", wavesrvBinName);
}
function getWaveSrvCwd(): string {
@ -71,7 +71,7 @@ function getWaveSrvCwd(): string {
export {
getElectronAppBasePath,
getGoAppBasePath,
getElectronAppUnpackedBasePath,
getWaveHomeDir,
getWaveSrvCwd,
getWaveSrvPath,

View File

@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld("api", {
getUserName: () => ipcRenderer.sendSync("get-user-name"),
getHostName: () => ipcRenderer.sendSync("get-host-name"),
getAboutModalDetails: () => ipcRenderer.sendSync("get-about-modal-details"),
getDocsiteUrl: () => ipcRenderer.sendSync("get-docsite-url"),
openNewWindow: () => ipcRenderer.send("open-new-window"),
showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position),
onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", (_event, id) => callback(id)),

View File

@ -3,15 +3,10 @@
.help-view {
width: 100%;
padding: 0 5px;
.title {
font-weight: bold;
}
code {
font: var(--fixed-font);
background-color: var(--highlight-bg-color);
padding: 0 5px;
height: 100%;
.docsite-webview {
width: 100%;
height: 100%;
border: 0;
}
}

View File

@ -1,208 +1,15 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
import { globalStore } from "@/app/store/global";
import { Atom, atom, PrimitiveAtom } from "jotai";
import { getApi } from "@/app/store/global";
import { useState } from "react";
import "./helpview.less";
const helpText = `
For more up-to-date documentation, please visit [our docs site](http://docs.waveterm.dev)
## Blocks
Every individual Component is contained in its own block. These can be added, removed, moved and resized. Each block has its own header which can be right clicked to reveal more operations you can do with that block.
### How to Add a Block
Adding a block can be done using the widget bar on the right hand side of the window. This will add a block of the selected type to the current tab.
### How to Close a Block
Blocks can be closed by clicking the &#x2715; button on the right side of the header. Alternatively, the currently focused block can be closed by pressing \`Cmd + w\`.
### How to Navigate Blocks
At most, it is possible to have one block be focused. Depending on the type of block, this allows you to directly interact with the content in that block. A focused block is always outlined with a distinct border. A block may be focused by clicking on it. Alternatively, you can change the focused block by pressing <code>Ctrl + Shift + &uarr;</code>, <code>Ctrl + Shift + &darr;</code>, <code>Ctrl + Shift + &larr;</code>, or <code>Ctrl + Shift + &rarr;</code>to navigate relative to the currently selected block.
1
### How to Magnify Blocks
Magnifying a block will pop the block out in front of everything else. You can magnify using the header icon, or with \`Cmd + m\`.
### How to Reorganize Blocks
By dragging and dropping their headers, blocks can be moved to different locations in the layout. This effectively allows you to reorganize your screen however you see fit. When dragging, you will see a preview of the block that is being dragged. When the block is over a valid drop point, the area where it would be moved to will turn green. Releasing the click will place the block there and reflow the other blocks around it. If you see a green box cover half of two different blocks, the drop will place the block between the two. If you see the green box cover half of one block at the edge of the screen, the block will be placed between that block and the edge of the screen. If you see the green box cover one block entirely, the two blocks will swap locations.
### How to Resize Blocks
Hovering the mouse between two blocks changes your cursor to &harr; and reveals a green line dividing the blocks. By dragging and dropping this green line, you are able to resize the blocks adjacent to it.
## Types of Blocks
### Term
The usual terminal you know and love. We add a few plugins via the \`wsh\` command that you can read more about further below.
### Preview
Preview is the generic type of block used for viewing files. This can take many different forms based on the type of file being viewed.
You can use \`wsh view [path]\` from any Wave terminal window to open a preview block with the contents of the specified path (e.g. \`wsh view .\` or \`wsh view ~/myimage.jpg\`).
#### Directory
When looking at a directory, preview will show a file viewer much like MacOS' *Finder* application or Windows' *File Explorer* application. This variant is slightly more geared toward software development with the focus on seeing what is shown by the \`ls -alh\` command.
##### View a New File
The simplest way to view a new file is to double click its row in the file viewer. Alternatively, while the block is focused, you can use the &uarr; and &darr; arrow keys to select a row and press enter to preview the associated file.
##### View the Parent Directory
In the directory view, this is as simple as opening the \`..\` file as if it were a regular file. This can be done with the method above. You can also use the keyboard shortcut \`Cmd + ArrowUp\`.
##### Navigate Back and Forward
When looking at a file, you can navigate back by clicking the back button in the block header or the keyboard shortcut \`Cmd + ArrowLeft\`. You can always navigate back and forward using \`Cmd + ArrowLeft\` and \`Cmd + ArrowRight\`.
##### Filter the List of Files
While the block is focused, you can filter by filename by typing a substring of the filename you're working for. To clear the filter, you can click the &#x2715; on the filter dropdown or press esc.
##### Sort by a File Column
To sort a file by a specific column, click on the header for that column. If you click the header again, it will reverse the sort order.
##### Hide and Show Hidden Files
At the right of the block header, there is an &#128065;&#65039; button. Clicking this button hides and shows hidden files.
##### Refresh the Directory
At the right of the block header, there is a refresh button. Clicking this button refreshes the directory contents.
##### Navigate to Common Directories
At the left of the block header, there is a file icon. Clicking and holding on this icon opens a menu where you can select a common folder to navigate to. The available options are *Home*, *Desktop*, *Downloads*, and *Root*.
##### Open a New Terminal in the Current Directory
If you right click the header of the block (alternatively, click the gear icon), one of the menu items listed is **Open Terminal in New Block**. This will create a new terminal block at your current directory.
##### Open a New Terminal in a Child Directory
If you want to open a terminal for a child directory instead, you can right click on that file's row to get the **Open Terminal in New Block** option. Clicking this will open a terminal at that directory. Note that this option is only available for children that are directories.
##### Open a New Preview for a Child
To open a new Preview Block for a Child, you can right click on that file's row and select the **Open Preview in New Block** option.
#### Markdown
Opening a markdown file will bring up a view of the rendered markdown. These files cannot be edited in the preview at this time.
#### Images/Media
Opening a picture will bring up the image of that picture. Opening a video will bring up a player that lets you watch the video.
### Codeedit
Opening most text files will open Codeedit to either view or edit the file. It is technically part of the Preview block, but it is important enough to be singled out.
After opening a codeedit block, it is often useful to magnify it (\`Cmd + m\`) to get a larger view. You can then
use the hotkeys below to switch to edit mode, make your edits, save, and then use \`Cmd + w\` to close the block (all without using the mouse!).
#### Switch to Edit Mode
To switch to edit mode, click the edit button to the right of the header. This lets you edit the file contents with a regular monaco editor.
You can also switch to edit mode by pressing \`Cmd + e\`.
#### Save an Edit
Once an edit has been made in **edit mode**, click the save button to the right of the header to save the contents.
You can also save by pressing \`Cmd + s\`.
#### Exit Edit Mode Without Saving
To exit **edit mode** without saving, click the cancel button to the right of the header.
You can also exit without saving by pressing \`Cmd + r\`.
### AI
#### How to Ask an LLM a Question
Asking a question is as simple as typing a message in the prompt and pressing enter. By default, we forward messages to the *gpt-4o-mini* model through our server.
#### How To Change The Model
See *settings help* for more info on how to configure your model.
### Web
The Web block is basically a simple web browser. The forward and backwards navigation have been added to the header.
You can use \`wsh\` to interact with the web block's URL (see the wsh section below).
### Cpu %
A small plot displaying the % of CPU in use over time. This is an example of a block that is capable of plotting streamed data. We plan to make this more generic in the future.
## Tabs
Tabs are ways to organize your blocks into separate screens. They mostly work the way you're used to in other apps.
### Create a New Tab
A tab can be created by clicking the plus button to the right of your currently existing tabs
### Delete a Tab
Hovering a tab reveals an &#x2715; button to the right side of it. Clicking it removes the tab. Note that this will also remove the instances of the blocks it contains.
### Change a Tab Name
Double clicking the current tab name makes it possible to change the name of your tab. You are limited to 10 glyphs in your tab name. Note that we say glyphs because it is possible to use multiple-character glyphs including emojis in your tab name.
### Reorganize Tabs
Tabs can be reorganized by dragging and dropping them to the left and right of other tabs.
## Theming
It is possible to style each tab individually. This is most-easily done by right clicking on your tab and going to the background menu. From there, you can select from five different pre-made styles.
It is possible to get more fine-grained control of the styles as well. See *settings help* for more info.
## wsh command
The wsh command is always available from wave terminal windows. It is a powerful tool for interacting with Wave blocks and can bridge data between your CLI and the widget GUIs.
### view
You can open a preview block with the contents of any file or directory by running:
\`\`\`
wsh view [path]
\`\`\`
You can use this command to easily preview images, markdown files, and directories. For code/text files this will open
a codeedit block which you can use to quickly edit the file using Wave's embedded graphical editor.
### edit
\`\`\`
wsh editor [path]
\`\`\`
This will open up codeedit for the specified file. This is useful for quickly editing files on a local or remote machine in our graphical editor. This command will wait until the file is closed before exiting (unlike \`view\`) so you can set your \`$EDITOR\` to \`wsh edit\` for a seamless experience. You can combine this with a \`-m\` flag to open the editor in magnified mode.
### getmeta
You can view the metadata of any block by running:
\`\`\`
wsh getmeta [blockid]
\`\`\`
This is especially useful for preview and web blocks as you can see the file or url that they are pointing to and use that in your CLI scripts.
### setmeta
You can update any metadata key value pair for blocks (and tabs) by using the setmeta command:
\`\`\`
wsh setmeta [blockid] [key]=[value]
wsh setmeta [blockid] file=~/myfile.txt
wsh setmeta [blockid] url=https://waveterm.dev/
\`\`\`
You can get block and tab ids by right clicking on the appropriate block and selecting "Copy BlockId". When you
update the metadata for a preview or web block you'll see the changes reflected instantly in the block.
Other useful metadata values to override block titles, icons, colors, themes, etc. can be found in the documentation.
`;
class HelpViewModel implements ViewModel {
viewType: string;
showTocAtom: PrimitiveAtom<boolean>;
endIconButtons: Atom<IconButtonDecl[]>;
constructor() {
this.viewType = "help";
this.showTocAtom = atom(false);
this.endIconButtons = atom([
{
elemtype: "iconbutton",
icon: "book",
title: "Table of Contents",
click: () => this.showTocToggle(),
},
] as IconButtonDecl[]);
}
showTocToggle() {
globalStore.set(this.showTocAtom, !globalStore.get(this.showTocAtom));
}
}
@ -210,8 +17,14 @@ function makeHelpViewModel() {
return new HelpViewModel();
}
function HelpView({ model }: { model: HelpViewModel }) {
return <Markdown text={helpText} showTocAtom={model.showTocAtom} className="help-view" />;
function HelpView({}: { model: HelpViewModel }) {
const [url] = useState(() => getApi().getDocsiteUrl());
console.log(url);
return (
<div className="help-view">
<webview className="docsite-webview" src={url} />
</div>
);
}
export { HelpView, HelpViewModel, makeHelpViewModel };

View File

@ -58,6 +58,7 @@ declare global {
getUserName: () => string;
getHostName: () => string;
getAboutModalDetails: () => AboutModalDetails;
getDocsiteUrl: () => string;
showContextMenu: (menu?: ElectronContextMenuItem[]) => void;
onContextMenuClick: (callback: (id: string) => void) => void;
onNavigate: (callback: (url: string) => void) => void;

29
pkg/docsite/docsite.go Normal file
View File

@ -0,0 +1,29 @@
package docsite
import (
"log"
"net/http"
"os"
"path/filepath"
"github.com/wavetermdev/waveterm/pkg/wavebase"
)
var docsiteStaticPath = filepath.Join(wavebase.GetWaveAppPath(), "docsite")
var docsiteHandler http.Handler
func GetDocsiteHandler() http.Handler {
stat, err := os.Stat(docsiteStaticPath)
if docsiteHandler == nil {
log.Println("Docsite is nil, initializing")
if err == nil && stat.IsDir() {
log.Printf("Found static site at %s, serving\n", docsiteStaticPath)
docsiteHandler = http.FileServer(http.Dir(docsiteStaticPath))
} else {
log.Println("Did not find static site, serving not found handler")
docsiteHandler = http.NotFoundHandler()
}
}
return docsiteHandler
}

View File

@ -32,9 +32,6 @@ var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
const DefaultShellPath = "/bin/bash"
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const AppPathBinDir = "bin"
const (
ZshIntegrationDir = "shell/zsh"
BashIntegrationDir = "shell/bash"
@ -231,7 +228,7 @@ func GetWshBaseName(version string, goos string, goarch string) string {
}
func GetWshBinaryPath(version string, goos string, goarch string) string {
return filepath.Join(os.Getenv(WaveAppPathVarName), AppPathBinDir, GetWshBaseName(version, goos, goarch))
return filepath.Join(wavebase.GetWaveAppBinPath(), GetWshBaseName(version, goos, goarch))
}
func InitRcFiles(waveHome string, wshBinDir string) error {
@ -319,7 +316,7 @@ func initCustomShellStartupFilesInternal() error {
if err != nil {
return fmt.Errorf("error copying wsh binary to bin: %v", err)
}
log.Printf("wsh binary successfully %q copied to %q\n", wshBaseName, wshDstPath)
log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath)
return nil
}

View File

@ -35,6 +35,9 @@ const WaveDBDir = "db"
const JwtSecret = "waveterm" // TODO generate and store this
const ConfigDir = "config"
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const AppPathBinDir = "bin"
var baseLock = &sync.Mutex{}
var ensureDirCache = map[string]bool{}
@ -43,6 +46,14 @@ func IsDevMode() bool {
return pdev != ""
}
func GetWaveAppPath() string {
return os.Getenv(WaveAppPathVarName)
}
func GetWaveAppBinPath() string {
return filepath.Join(GetWaveAppPath(), AppPathBinDir)
}
func GetHomeDir() string {
homeVar, err := os.UserHomeDir()
if err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/docsite"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/telemetry"
@ -441,6 +442,8 @@ func MakeUnixListener() (net.Listener, error) {
return rtn, nil
}
const docsitePrefix = "/docsite/"
// blocking
func RunWebServer(listener net.Listener) {
gr := mux.NewRouter()
@ -448,6 +451,7 @@ func RunWebServer(listener net.Listener) {
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
gr.HandleFunc("/wave/log-active-state", WebFnWrap(WebFnOpts{JsonErrors: true}, handleLogActiveState))
gr.PathPrefix(docsitePrefix).Handler(http.StripPrefix(docsitePrefix, docsite.GetDocsiteHandler()))
handler := http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout")
if wavebase.IsDevMode() {
handler = handlers.CORS(handlers.AllowedOrigins([]string{"*"}))(handler)