1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into file-menu-reorder

This commit is contained in:
Lauri Nevala 2020-11-18 09:52:01 +02:00
commit 660d4016bf
26 changed files with 304 additions and 35924 deletions

View File

@ -36,11 +36,11 @@ jobs:
yarn
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- script: make install-deps
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
- script: make -j2 build-extensions
displayName: Build bundled extensions
- script: make integration-win
displayName: Run integration tests
@ -76,11 +76,11 @@ jobs:
yarn
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- script: make install-deps
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
- script: make -j2 build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
@ -122,13 +122,13 @@ jobs:
yarn
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- script: make install-deps
- script: make node_modules
displayName: Install dependencies
- script: make lint
displayName: Lint
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
- script: make -j2 build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
@ -164,4 +164,4 @@ jobs:
displayName: Publish npm package
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
NPM_TOKEN: $(NPM_TOKEN)
NPM_TOKEN: $(NPM_TOKEN)

View File

@ -3,7 +3,9 @@ on:
push:
branches:
- master
release:
types:
- published
jobs:
build:
name: Deploy docs
@ -27,13 +29,12 @@ jobs:
- name: Checkout Release from lens
uses: actions/checkout@v2
with:
repository: lensapp/lens
fetch-depth: 0
- name: git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git pull
- name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
@ -45,17 +46,19 @@ jobs:
yarn install
yarn typedocs-extensions-api
- name: mkdocs deploy latest
- name: mkdocs deploy master
if: contains(github.ref, 'refs/heads/master')
run: |
mike deploy --push latest
mike deploy --push master
- name: mkdocs deploy new release / tag
if: contains(github.ref, 'refs/tags/v')
- name: Get the release version
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: mkdocs deploy new release
if: contains(github.ref, 'refs/tags/v') # && !github.event.release.prerelease (generate pre-release docs until Lens 4.0.0 is GA, see #1408)
run: |
mike deploy --push--update-aliases ${{ github.ref }} latest
mike set-default --push ${{ github.ref }}
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
mike set-default --push ${{ steps.get_version.outputs.VERSION }}

View File

@ -0,0 +1,38 @@
name: Delete Documentation Version
on:
workflow_dispatch:
inputs:
version:
description: 'Version string to be deleted (e.g."v0.0.1")'
required: true
jobs:
build:
name: Delete docs Version
runs-on: ubuntu-latest
steps:
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install git+https://${{ secrets.GH_TOKEN }}@github.com/lensapp/mkdocs-material-insiders.git
pip install mike
- name: Checkout Release from lens
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: mkdocs delete version
run: |
mike delete --push ${{ github.event.inputs.version }}

View File

@ -0,0 +1,38 @@
name: Update Default Documentation Version
on:
workflow_dispatch:
inputs:
version:
description: 'Version string to be default (e.g."v0.0.1")'
required: true
jobs:
build:
name: Update default docs Version
runs-on: ubuntu-latest
steps:
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install git+https://${{ secrets.GH_TOKEN }}@github.com/lensapp/mkdocs-material-insiders.git
pip install mike
- name: Checkout Release from lens
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: mkdocs update default version
run: |
mike set-default --push ${{ github.event.inputs.version }}

View File

@ -1,4 +1,7 @@
EXTENSIONS_DIR = ./extensions
extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir})
extension_node_modules = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/node_modules)
extension_dists = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}/dist)
ifeq ($(OS),Windows_NT)
DETECTED_OS := Windows
@ -6,29 +9,23 @@ else
DETECTED_OS := $(shell uname)
endif
.PHONY: init
init: install-deps download-bins compile-dev
echo "Init done"
.PHONY: download-bins
download-bins:
binaries/client:
yarn download-bins
.PHONY: install-deps
install-deps:
node_modules:
yarn install --frozen-lockfile --verbose
yarn check --verify-tree --integrity
static/build/LensDev.html:
yarn compile:renderer
.PHONY: compile-dev
compile-dev:
yarn compile:main --cache
yarn compile:renderer --cache
.PHONY: dev
dev:
ifeq ("$(wildcard static/build/main.js)","")
make init
endif
dev: node_modules binaries/client build-extensions static/build/LensDev.html
yarn dev
.PHONY: lint
@ -36,7 +33,7 @@ lint:
yarn lint
.PHONY: test
test: download-bins
test: binaries/client
yarn test
.PHONY: integration-linux
@ -59,20 +56,25 @@ test-app:
yarn test
.PHONY: build
build: install-deps download-bins build-extensions
build: node_modules binaries/client build-extensions
ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win
else
yarn dist
endif
$(extension_node_modules):
cd $(@:/node_modules=) && npm install --no-audit --no-fund
$(extension_dists): src/extensions/npm/extensions/dist
cd $(@:/dist=) && npm run build
.PHONY: build-extensions
build-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), (cd $(dir) && npm install && npm run build || exit $?);)
build-extensions: $(extension_node_modules) $(extension_dists)
.PHONY: test-extensions
test-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), (cd $(dir) && npm install --dev && npm run test || exit $?);)
test-extensions: $(extension_node_modules)
$(foreach dir, $(extensions), (cd $(dir) && npm run test || exit $?);)
.PHONY: copy-extension-themes
copy-extension-themes:
@ -97,6 +99,16 @@ publish-npm: build-npm
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
cd src/extensions/npm/extensions && npm publish --access=public
.PHONY: clean-extensions
clean-extensions:
ifeq "$(DETECTED_OS)" "Windows"
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), if exist $(dir)\dist del /s /q $(dir)\dist)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), if exist $(dir)\node_modules del /s /q $(dir)\node_modules)
else
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
endif
.PHONY: clean-npm
clean-npm:
ifeq "$(DETECTED_OS)" "Windows"
@ -110,13 +122,13 @@ else
endif
.PHONY: clean
clean: clean-npm
clean: clean-npm clean-extensions
ifeq "$(DETECTED_OS)" "Windows"
if exist binaries\client del /s /q binaries\client\*.*
if exist binaries\client del /s /q binaries\client
if exist dist del /s /q dist\*.*
if exist static\build del /s /q static\build\*.*
else
rm -rf binaries/client/*
rm -rf binaries/client
rm -rf dist/*
rm -rf static/build/*
endif

View File

@ -36,7 +36,6 @@ brew cask install lens
> Prerequisites: Nodejs v12, make, yarn
* `make init` - initial compilation, installing deps, etc.
* `make dev` - builds and starts the app
* `make test` - run tests

View File

@ -0,0 +1,28 @@
# Extension Guides
The basics of the Lens Extension API are covered in [Your First Extension](../get-started/your-first-extension.md). In this section detailed code guides and samples are used to explain how to use specific Lens Extension APIs.
Each guide or sample will include:
- Clearly commented source code.
- Instructions for running the sample extension.
- Image of the sample extension's appearance and usage.
- Listing of Extension API being used.
- Explanation of Extension API concepts.
## Guides
| Guide | APIs |
| ----- | ----- |
| [Main process extension](main-extension.md) | LensMainExtension |
| [Renderer process extension](renderer-extension.md) | LensRendererExtension |
| [Stores](stores.md) | |
| [Components](components.md) | |
| [KubeObjectListLayout](kube-object-list-layout.md) | |
## Samples
| Sample | APIs |
| ----- | ----- |
[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |

View File

@ -0,0 +1,76 @@
# Main Extension
The main extension api is the interface to Lens' main process (Lens runs in main and renderer processes). It allows you to access, configure, and customize Lens data, add custom application menu items, and generally run custom code in Lens' main process.
## `LensMainExtension` Class
To create a main extension simply extend the `LensMainExtension` class:
``` typescript
import { LensMainExtension } from "@k8slens/extensions";
export default class ExampleExtensionMain extends LensMainExtension {
onActivate() {
console.log('custom main process extension code started');
}
onDeactivate() {
console.log('custom main process extension de-activated');
}
}
```
There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By overriding `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled. Note that to see standard output from the main process there must be a console connected to it. This is typically achieved by starting Lens from the command prompt.
The following example is a little more interesting in that it accesses some Lens state data and periodically logs the name of the currently active cluster in Lens.
``` typescript
import { LensMainExtension, Store } from "@k8slens/extensions";
const clusterStore = Store.clusterStore
export default class ActiveClusterExtensionMain extends LensMainExtension {
timer: NodeJS.Timeout
onActivate() {
console.log("Cluster logger activated");
this.timer = setInterval(() => {
if (!clusterStore.active) {
console.log("No active cluster");
return;
}
console.log("active cluster is", clusterStore.active.contextName)
}, 5000)
}
onDeactivate() {
clearInterval(this.timer)
console.log("Cluster logger deactivated");
}
}
```
See the [Stores](../stores) guide for more details on accessing Lens state data.
### `appMenus`
The only UI feature customizable in the main extension api is the application menu. Custom menu items can be inserted and linked to custom functionality, such as navigating to a specific page. The following example demonstrates adding a menu item to the Help menu.
``` typescript
import { LensMainExtension } from "@k8slens/extensions";
export default class SamplePageMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "Sample",
click() {
console.log("Sample clicked");
}
}
]
}
```
`appMenus` is an array of objects satisfying the `MenuRegistration` interface. `MenuRegistration` extends React's `MenuItemConstructorOptions` interface. `parentId` is the id of the menu to put this menu item under (todo: is this case sensitive and how do we know what the available ids are?), `label` is the text to show on the menu item, and `click()` is called when the menu item is selected. In this example we simply log a message, but typically you would navigate to a specific page or perform some operation. Pages are associated with the [`LensRendererExtension`](renderer-extension.md) class and can be defined when you extend it.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,7 @@ nav:
- Color Reference: extensions/capabilities/color-reference.md
- Extension Guides:
- Overview: extensions/guides/README.md
- Main Extension: extensions/guides/main-extension.md
- Renderer Extension: extensions/guides/renderer-extension.md
- Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md

View File

@ -38,15 +38,18 @@ describe("workspace store tests", () => {
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
})
it("can update default workspace name", () => {
it("can update workspace description", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.addWorkspace(new Workspace({
id: WorkspaceStore.defaultId,
const workspace = ws.addWorkspace(new Workspace({
id: "foobar",
name: "foobar",
}));
expect(ws.currentWorkspace.name).toBe("foobar");
workspace.description = "Foobar description";
ws.updateWorkspace(workspace);
expect(ws.getById("foobar").description).toBe("Foobar description");
})
it("can add workspaces", () => {

View File

@ -153,20 +153,20 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@action
addWorkspace(workspace: Workspace) {
const { id, name } = workspace;
const existingWorkspace = this.getById(id);
if (!name.trim() || this.getByName(name.trim())) {
return;
}
if (existingWorkspace) {
Object.assign(existingWorkspace, workspace);
appEventBus.emit({name: "workspace", action: "update"})
} else {
appEventBus.emit({name: "workspace", action: "add"})
}
this.workspaces.set(id, workspace);
appEventBus.emit({name: "workspace", action: "add"})
return workspace;
}
@action
updateWorkspace(workspace: Workspace) {
this.workspaces.set(workspace.id, workspace);
appEventBus.emit({name: "workspace", action: "update"});
}
@action
removeWorkspace(workspace: Workspace) {
this.removeWorkspaceById(workspace.id)

View File

@ -86,6 +86,10 @@ export class Cluster implements ClusterModel, ClusterState {
return this.accessible && !this.disconnected;
}
@computed get name() {
return this.preferences.clusterName || this.contextName
}
get version(): string {
return String(this.metadata?.version) || ""
}

View File

@ -9,8 +9,8 @@ export function exitApp() {
const windowManager = WindowManager.getInstance<WindowManager>()
const clusterManager = ClusterManager.getInstance<ClusterManager>()
appEventBus.emit({ name: "service", action: "close" })
windowManager?.hide();
clusterManager?.stop();
windowManager.hide();
clusterManager.stop();
logger.info('SERVICE:QUIT');
setTimeout(() => {
app.exit()

View File

@ -89,7 +89,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(cluster => {
const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
const { id: clusterId, name: label, online, workspace } = cluster;
return {
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
toolTip: clusterId,

View File

@ -20,7 +20,7 @@ export class Workspaces extends React.Component {
@computed get workspaces(): Workspace[] {
const currentWorkspaces: Map<WorkspaceId, Workspace> = new Map()
workspaceStore.enabledWorkspacesList.forEach((w) => {
workspaceStore.workspacesList.forEach((w) => {
currentWorkspaces.set(w.id, w)
})
const allWorkspaces = new Map([
@ -45,9 +45,13 @@ export class Workspaces extends React.Component {
}
saveWorkspace = (id: WorkspaceId) => {
const draft = toJS(this.editingWorkspaces.get(id));
const workspace = workspaceStore.addWorkspace(draft);
if (workspace) {
const workspace = new Workspace(this.editingWorkspaces.get(id));
if (workspaceStore.getById(id)) {
workspaceStore.updateWorkspace(workspace);
this.clearEditing(id);
return;
}
if (workspaceStore.addWorkspace(workspace)) {
this.clearEditing(id);
}
}
@ -127,7 +131,7 @@ export class Workspaces extends React.Component {
validate: value => !workspaceStore.getByName(value.trim())
}
return (
<div key={workspaceId} className={className}>
<div key={workspaceId} className={cssNames(className)}>
{!isEditing && (
<Fragment>
<span className="name flex gaps align-center">

View File

@ -34,8 +34,8 @@ export class ClusterIcon extends React.Component<Props> {
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
children, ...elemProps
} = this.props;
const { isAdmin, eventCount, preferences, id: clusterId } = cluster;
const { clusterName, icon } = preferences;
const { isAdmin, name, eventCount, preferences, id: clusterId } = cluster;
const { icon } = preferences;
const clusterIconId = `cluster-icon-${clusterId}`;
const className = cssNames("ClusterIcon flex inline", this.props.className, {
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
@ -44,9 +44,9 @@ export class ClusterIcon extends React.Component<Props> {
return (
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
{showTooltip && (
<Tooltip targetId={clusterIconId}>{clusterName}</Tooltip>
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
)}
{icon && <img src={icon} alt={clusterName}/>}
{icon && <img src={icon} alt={name}/>}
{!icon && <Hashicon value={clusterId} options={options}/>}
{showErrors && isAdmin && eventCount > 0 && (
<Badge

View File

@ -1,4 +1,7 @@
.PodLogs {
--overlay-bg: #8cc474b8;
--overlay-active-bg: orange;
.logs {
@include custom-scrollbar;
@ -11,14 +14,6 @@
background: $logsBackground;
flex-grow: 1;
.find-overlay {
position: absolute;
border-radius: 2px;
background-color: #8cc474;
margin-top: 4px;
opacity: 0.5;
}
.VirtualList {
height: 100%;
@ -29,19 +24,30 @@
font-family: $font-monospace;
font-size: smaller;
white-space: pre;
-webkit-font-smoothing: auto; // Better readability on non-retina screens
&:hover {
background: $logRowHoverBackground;
}
span {
-webkit-font-smoothing: auto; // Better readability on non-retina screens
}
span.overlay {
border-radius: 2px;
background-color: #8cc474b8;
-webkit-font-smoothing: auto;
background-color: var(--overlay-bg);
span {
background-color: var(--overlay-bg)!important; // Rewriting inline styles from AnsiUp library
}
&.active {
background-color: orange;
background-color: var(--overlay-active-bg);
span {
background-color: var(--overlay-active-bg)!important; // Rewriting inline styles from AnsiUp library
}
}
}
}
@ -49,25 +55,6 @@
}
}
.new-logs-sep {
position: relative;
display: block;
height: 0;
border-top: 1px solid $primary;
margin: $margin * 2 0;
&:after {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
content: 'new';
background: $primary;
color: white;
padding: $padding / 3;
border-radius: $radius;
}
}
.jump-to-bottom {
position: absolute;
right: 30px;

View File

@ -1,5 +1,7 @@
import "./pod-logs.scss";
import React from "react";
import AnsiUp from 'ansi_up';
import DOMPurify from "dompurify"
import { Trans } from "@lingui/macro";
import { action, computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
@ -33,6 +35,7 @@ export class PodLogs extends React.Component<Props> {
private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
private colorConverter = new AnsiUp();
componentDidMount() {
disposeOnUnmount(this, [
@ -185,6 +188,7 @@ export class PodLogs extends React.Component<Props> {
const { searchQuery, isActiveOverlay } = searchStore;
const item = this.logs[rowIndex];
const contents: React.ReactElement[] = [];
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(this.colorConverter.ansi_to_html(ansi));
if (searchQuery) { // If search is enabled, replace keyword with backgrounded <span>
// Case-insensitive search (lowercasing query and keywords in line)
const regex = new RegExp(searchStore.escapeRegex(searchQuery), "gi");
@ -195,19 +199,26 @@ export class PodLogs extends React.Component<Props> {
pieces.forEach((piece, index) => {
const active = isActiveOverlay(rowIndex, index);
const lastItem = index === pieces.length - 1;
const overlayValue = matches.next().value;
const overlay = !lastItem ?
<span className={cssNames({ active })}>{matches.next().value}</span> :
<span
className={cssNames("overlay", { active })}
dangerouslySetInnerHTML={{ __html: ansiToHtml(overlayValue) }}
/> :
null
contents.push(
<React.Fragment key={piece + index}>
{piece}{overlay}
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(piece) }} />
{overlay}
</React.Fragment>
);
})
}
return (
<div className={cssNames("LogRow")}>
{contents.length > 1 ? contents : item}
{contents.length > 1 ? contents : (
<span dangerouslySetInnerHTML={{ __html: ansiToHtml(item) }} />
)}
</div>
);
}

View File

@ -65,7 +65,7 @@ export class MainLayout extends React.Component<MainLayoutProps> {
return (
<div className={cssNames("MainLayout", className)} style={this.getSidebarSize() as any}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
<span className="cluster">{cluster.name}</span>
</header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>