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

Merge branch 'master' into resource-status-text-extension

This commit is contained in:
Lauri Nevala 2020-11-10 09:39:44 +02:00
commit 8f120c17f3
176 changed files with 5725 additions and 2309 deletions

View File

@ -37,6 +37,10 @@ jobs:
displayName: Cache Yarn packages
- script: make install-deps
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make integration-win
displayName: Run integration tests
- script: make test-extensions
@ -76,6 +80,10 @@ jobs:
condition: eq(variables.CACHE_RESTORED, 'true')
- script: make install-deps
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
- script: make integration-mac
@ -127,6 +135,10 @@ jobs:
displayName: Run In-tree Extension tests
- script: make lint
displayName: Lint
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
- bash: |

View File

@ -1,17 +0,0 @@
---
area/ui:
- src/renderer/**/*
area/test:
- integration/**/*
- __mocks__/**/*
area/extension:
- extensions/**/*
- src/extensions/**/*
area/documentation:
- README.md
- docs/**/*
area/ci:
- .github/workflows/**/*
- .azure-pipelines.yml
dependencies:
- yarn.lock

View File

@ -1,15 +0,0 @@
---
name: "Pull Request Labeler"
on:
- pull_request
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2
if: github.repository == 'lensapp/lens'
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler-config.yml

49
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- master
jobs:
build:
name: Deploy docs
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 mkdocs-git-revision-date-localized-plugin mike
- name: Checkout Release from lens
uses: actions/checkout@v2
with:
repository: lensapp/lens
- name: git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git pull
- name: mkdocs deploy latest
run: |
mike deploy --push latest
- name: mkdocs deploy new release / tag
if: contains(github.ref, 'refs/tags/v')
run: |
mike deploy --push--update-aliases ${{ github.ref }} latest
mike set-default --push ${{ github.ref }}

View File

@ -45,6 +45,16 @@ Allows for faster separate re-runs of some of the more involved processes:
1. `yarn dev:extension-types` compile declaration types for `@k8slens/extensions`
1. `yarn dev-run` runs app in dev-mode and auto-restart when main process file has changed
## Development (documentation)
Run a local instance of `mkdocs serve` in a docker container for developing the Lens Documentation.
> Prerequisites: docker, yarn
* `yarn mkdocs-serve-local` - local build and serve of mkdocs with auto update enabled
Go to [localhost:8000](http://127.0.0.1:8000)
## Developer's ~~RTFM~~ recommended list:
- [TypeScript](https://www.typescriptlang.org/docs/home.html) (front-end/back-end)
@ -53,6 +63,8 @@ Allows for faster separate re-runs of some of the more involved processes:
- [ElectronJS](https://www.electronjs.org/docs) (chrome/node)
- [NodeJS](https://nodejs.org/dist/latest-v12.x/docs/api/) (api docs)
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens.

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
docs.k8slens.dev

21
docs/README.md Normal file
View File

@ -0,0 +1,21 @@
# Overview
Lens is the most powerful Kubernetes IDE on the market. It is a standalone application, and it is available on macOS, Windows, and Linux. Some of the benefits of using Lens include:
* Confidence that your clusters are properly setup and configured.
* Increased visibility, real time statistics, log streams, and hands-on troubleshooting capabilities.
* The ability to work with your clusters quickly and easily, radically improving productivity and the speed of business.
Watch this introductory video to see Lens in action:
[![Screenshot](img/lens-intro-video-screenshot.png)](https://youtu.be/04v2ODsmtIs)
**Note:** Use CTRL+click (on Windows and Linux) or CMD+click (on MacOS) to open the above in a new tab
## Downloading Lens
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.
## Quick Start
Get up and running quickly by learning to [add clusters](clusters/adding-clusters.md).

View File

@ -0,0 +1,16 @@
# Adding clusters
Add clusters by clicking the **Add Cluster** button in the left-side menu.
1. Click the **Add Cluster** button (indicated with a '+' icon).
2. Enter the path to your kubeconfig file. You'll need to have a kubeconfig file for the cluster you want to add. You can either browse for the path from the file system or or enter it directly.
Selected [cluster contexts](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) are added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.
**NOTE**: Any cluster that you added manually will not be merged into your kubeconfig file.
![Add Cluster](images/add-cluster.png)
For more information on kubeconfig see [Kubernetes docs](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/).
To see your currently-enabled config with `kubectl`, enter `kubectl config view --minify --raw` in your terminal.

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,12 @@
# Removing clusters
Remove Lens clusters using the context menu that appears when you right-click the cluster in the left-side menu that you want to remove.
To remove a cluster from your cluster list:
1. Right-click the name of the cluster in the left-side menu that you want to remove.
2. Click **Remove**.
**NOTE**: This will only remove the cluster from your Lens cluster list. It will not affect your actual Kubernetes cluster or its configuration.
![Remove Cluster](images/remove-cluster.png)

75
docs/clusters/settings.md Normal file
View File

@ -0,0 +1,75 @@
# Settings
It is easy to configure Lens Clusters to your liking through its various settings.
1. Right-click the name of the cluster in the left-side menu that you want to open the settings for.
2. Click **Settings**.
![Cluster settings](images/cluster-context-menu.png)
## Status
Overview of the cluster status
### Cluster Status
Cluster status information including the detected distribution, kernel version, API endpoint, and online status
![Cluster settings status](images/cluster-settings-status.png)
## General
General cluster settings
### Cluster Name
The cluster name is inheritated by default from the kubeconfig file. Change the cluster name to another value by updating it here. Note that doing so does not update your kubeconfig file.
### Workspace
This is the Lens workspace that the cluster is associated with. Change workspaces by selecting a different workspace from the dropdown menu. Create a new workspace by clicking **workspace** in "Define cluster workspace" above the dropdown menu. This option will take you the workspaces editor. Create a new workspace and then navigate back to cluster settings.
### Cluster Icon
Lens randomly generates an icon to associate with each newly-created cluster. Use this setting to choose your own icon.
### HTTP Proxy
Some users will need to define an HTTP proxy for communicating with the Kubernetes API. Use this setting to do so.
### Prometheus
Lens can be configured to query a Prometheus server installed in the cluster. Select a query format by choosing either to auto-detect or from the following configurations:
* Lens
* Helm Operator
* Prometheus Operator
* Stacklight
To learn more about custom Prometheus configurations, please refer to this [guide](https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md).
### Working Directory
Use this field to set the terminal working directory. The default is `$HOME`.
![Cluster settings general](images/cluster-settings-general.png)
## Features
Additional Lens features that can be installed by the user
### Metrics
Enable timeseries data visualization (Prometheus stack) for your cluster. Install this only if you don't have existing Prometheus stack installed.
### User Mode
User Mode feature enables non-admin users to see namespaces they have access to. This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
![Cluster settings features](images/cluster-settings-features.png)
## Removal
Use this setting to remove the current cluster.
![Cluster settings removal](images/cluster-settings-removal.png)

View File

@ -0,0 +1,17 @@
# Contributing
There are multiple ways you can contribute to Lens - even if you are not a developer, you can still contribute. We are always looking for assistance with creating or updating documentation, testing the application, reporting and troubleshooting issues.
Here are some ideas how you can contribute!
* [Development](./development.md) Help making Lens better.
* [Maintaining the Project](./maintainers.md) Become community maintainer and help us maintain the project.
* [Extension Development](../extensions) Add integrations via Lens Extensions.
* [Documentation](./documentation.md) Help improve Lens documentation.
* [Promotion](./promotion.md) Show your support, be an ambassador to Lens, write blogs and make videos!
If you are an influencer, blogger or journalist, feel free to [spread the word](./promotion.md)!
## Code of Conduct
This project adheres to the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct. By participating and contributing to Lens, you are expected to uphold this code. Please report unacceptable behaviour to info@k8slens.dev

View File

@ -0,0 +1,3 @@
# Development
TBD

View File

@ -0,0 +1,22 @@
# Documentation
We are glad to see you are interested in contributing to Lens documentation. If this is the first Open Source project you will contribute to, we strongly suggest reading GitHub's excellent guide: [How to Contribute to Open Source](https://opensource.guide/how-to-contribute).
## Finding Documentation Issues to Work On
You can find a list of open documentation related issues [here](https://github.com/lensapp/lens/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Fdocumentation). When you find something you would like to work on:
1. Express your interest to start working on an issue via comments.
2. One of the maintainers will assign the issue for you.
3. You can start working on the issue. Once done, simply submit a pull request.
## Requirements for Documentation Pull Requests
When you create a new pull request, we expect some requirements to be met.
* Follow this naming convention for Pull Requests:
* When adding new documentation, add `New Documentation:` before the title. E.g. `New Documentation: Getting Started`
* When fixing documentation, add `Fix Documentation:` before the title. E.g. `Fix Documentation: Getting Started`
* When updating documentation, add `Update Documentation:` before the title. E.g. `Update Documentation: Getting Started`
* If your Pull Request closes an issue you need to write `Closes #ISSUE_NUMBER` where the ISSUE_NUMBER is the number in the end of the link url that will link your pull request to the issue, when merged will close that issue.
* For each pull request made, we run tests to check if there are any broken links, the markdown formatting is valid and the linter is passing.

View File

@ -0,0 +1,17 @@
# Maintainers
We are looking for community maintainers for the Lens project. Maintainers will be added to a special team with write permissions. These permissions consist of opening, closing, tagging and editing issues, and pull requests, as well as create and delete non protected branches.
The responsibilities of a community maintainer are listed below.
## Issues Triage
* **Labeling Issues:** Label issues accordingly.
* **Finding Duplicates:** Finding and closing duplicate issues.
* **Doing First Level Contact:** Getting more information on the issues (like version number or asking for clarification) if needed.
* **Closing Irrelevant Issues:** Closing issues that are determined irrelevant, no longer needed, not relevant to the project and/or doesn't follow the issues guidelines.
## Help with Contributions
* **Help Manage Pull Requests:** Help the author of the pull request with any problems.
* **Contributing:** Create pull requests to help maintain and drive the project forward.

View File

@ -0,0 +1,27 @@
# Promoting
Help promote Lens! If you are not a developer (or even if you are), you can still contribute to the project, a lot, by helping us promote it. As we are a free open source project, the community is our most important asset, so here are some ways that you can help the project continue to grow.
## Follow, Like, Recommend, Favorite, Vote and Star Us
There are many sites where you can vote, recommend, favorite and star us.
* [Twitter](https://twitter.com/k8slens) - Like, comment and retweet our posts, and follow us on Twitter
* [Medium](https://medium.com/k8slens) - Give claps to our articles and follow us on Medium
* [GitHub](https://github.com/lensapp/lens) - Become a stargazer on GitHub
* [StackShare](https://stackshare.io/lens) - Indicate you are using Lens and follow us on StackShare
* [Reddit](https://www.reddit.com/search/?q=lens%20kubernetes&sort=new) - Upvote and be an ambassador of Lens by participating relevant discussions on Reddit
* [Hacker News](https://hn.algolia.com/?dateRange=all&page=0&prefix=false&query=lens%20kubernetes&sort=byDate&type=story) - Upvote and be an ambassador of Lens by participating relevant discussions on Hacker News
## Write Blogs or Make Videos About Us
Here are some nice blog posts and videos about our project for you to get some inspiration:
* [Onboard AWS EKS Cluster on Lens(Kubernetes IDE)](https://dev.to/himwad05/onboard-aws-eks-cluster-on-lens-kubernetes-ide-492l)
* [Using Lens to Manage All Your Kubernetes Cluster](https://medium.com/@magicmagnate/using-lens-to-manage-all-your-kubernetes-cluster-c1ef88fdb476)
* [Kontena Lens - Beautiful Kubernetes UI](https://www.youtube.com/watch?v=YGgaiGdYfdI)
* [Gerenciando Kubernetes com Lens e Octant](https://www.youtube.com/watch?v=h9ZqDelJLQQ)
* [Walkthrough of Kubernetes IDE - Lens](https://www.youtube.com/watch?v=602aHZSdEfY)
* [LENS - Interfaz Gráfica para Kubernetes en 1 PASO.](https://www.youtube.com/watch?v=DFMKcR4BqwM)
Psst... If you have created some content around Lens, let us know!

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block analytics %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-90E1JB4HW4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-90E1JB4HW4');
</script>
{% endblock %}

48
docs/extensions/README.md Normal file
View File

@ -0,0 +1,48 @@
# Lens Extension API
The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core features of Lens are built as extensions and use the same Extension API.
This documentation describes:
* How to build, run, test and publish an extension
* How to take advantage of Lens's Extension API
* Where to find [guides](guides/overview.md) and [code samples](https://github.com/lensapp/lens-extension-samples) to help get you started
Code samples are available at [lensapp/lens-extension-samples](https://github.com/lensapp/lens-extension-samples).
## What can extensions do
Here are some examples of what you can achieve with the Extension API:
* Add custom components & views in the UI - Extending the Lens Workbench
If you'd like to have a more comprehensive overview of the Extension API, refer to the [Extension Capabilities Overview](capabilities/overview.md) page. [Extension Guides Overview](guides/overview.md) also includes a list of code samples and guides that illustrate various Extension API usage.
## How to build extensions
Building a good extension can take a lot of effort. Here is what each section of the API doc can help you with:
* **Get Started** teaches fundamental concepts for building extensions with the Hello World sample.
* **Extension Capabilities** dissects Lens's Extension API into smaller categories and points you to more detailed topics.
* **Extension Guides** includes guides and code samples that explain specific usages of Lens Extension API.
* **Testing and Publishing** includes in-depth guides on various extension development topics, such as testing and publishing extensions.
* **Advanced Topics** explains advanced concepts such as integrating with 3rd party applications/services.
* **References** contains exhaustive references for the Lens Extension API, Contribution Points, and many other topics.
## What's new
Lens updates on a monthly cadence, and that applies to the Extension API as well. New features and APIs become available every month to increase the power and scope of Lens extensions.
To stay current with the Extension API and Lens features in general, you can review the [release notes](https://github.com/lensapp/lens/releases).
## Looking for help
If you have questions for extension development, try asking on:
* [Lens Dev Slack](http://k8slens.slack.com/): Public chatroom for Lens developers. Some Lens team members chime in on conversations.
To provide feedback on the documentation or issues with the Lens Extension API, create new issues at [lensapp/lens](https://github.com/lensapp/lens/issues) with the labels `area/documentation` and/or `area/extension`.
## Download Lens - The Kubernetes IDE
Go to [Lens](https://k8slens.dev)

View File

@ -0,0 +1,3 @@
# Lens Extension API Reference
TBD

View File

View File

@ -0,0 +1,130 @@
# Theme color reference
You can use CSS variables generated from theme `.json` files to style an extension with respect of active theme.
## Base colors
- `blue`: blue color.
- `magenta`: magenta color.
- `golden`: gold/yellow color.
- `halfGray`: gray with some apacity applied.
- `primary`: Lens brand (blue) color.
- `colorSuccess`: successfull operations color.
- `colorOk`: successfull operations (bright version) color.
- `colorInfo`: informational, in-progress color.
- `colorError`: critical error color.
- `colorSoftError`: error color.
- `colorWarning`: warning color.
- `colorVague`: soft gray color for notices, hints etc.
- `colorTerminated`: terminated, closed, stale color.
- `boxShadow`: semi-transparent box-shadow color.
## Text colors
- `textColorPrimary`: foreground text color.
- `textColorSecondary`: foreground text color for different paragraps, parts of text.
- `textColorAccent`: foreground text color to highlight its parts.
## Border colors
- `borderColor`: border color.
- `borderFaintColor`: fainted (lighter or darker, which depends on the theme) border color.
## Layout colors
- `mainBackground`: main background color for the app.
- `contentColor`: background color for panels contains some data.
- `layoutBackground`: background color for layout parts.
- `layoutTabsBackground`: background color for general tabs.
- `layoutTabsActiveColor`: foreground color for general tabs.
- `layoutTabsLineColor`: background color for lines under general tabs.
## Sidebar colors
- `sidebarLogoBackground`: background color behind logo in sidebar.
- `sidebarActiveColor`: foreground color for active menu items in sidebar.
- `sidebarSubmenuActiveColor`: foreground color for active submenu items in sidebar.
- `sidebarBackground`: background color for sidebar.
## Button colors
- `buttonPrimaryBackground`: button background color for primary actions.
- `buttonDefaultBackground`: default button background color.
- `buttonAccentBackground`: accent button background color.
- `buttonDisabledBackground`: disabled button background color.
## Table colors
- `tableBgcStripe`: background color for odd rows in table.
- `tableBgcSelected`: background color for selected row in table.
- `tableHeaderBackground`: background color for table header.
- `tableHeaderBorderWidth`: border width under table header.
- `tableHeaderBorderColor`: border color for line under table header.
- `tableHeaderColor`: foreground color for table header.
- `tableSelectedRowColor`: foreground color for selected row in table.
## Dock colors
- `dockHeadBackground`: background color for dock's header.
- `dockInfoBackground`: background color for dock's info panel.
- `dockInfoBorderColor`: border color for dock's info panel.
## Helm chart colors
- `helmLogoBackground`: background color for chart logo.
- `helmImgBackground`: background color for chart image.
- `helmStableRepo`: background color for stable repo.
- `helmIncubatorRepo`: background color for incubator repo.
- `helmDescriptionHr`: Helm chart description separator line color.
- `helmDescriptionBlockqouteColor`: Helm chart description blockquote color.
- `helmDescriptionBlockqouteBorder`: Helm chart description blockquote border color.
- `helmDescriptionBlockquoteBackground`: Helm chart description blockquote background color.
- `helmDescriptionHeaders`: Helm chart description headers color.
- `helmDescriptionH6`: Helm chart description header foreground color.
- `helmDescriptionTdBorder`: Helm chart description table cell border color.
- `helmDescriptionTrBackground`: Helm chart description table row background color.
- `helmDescriptionCodeBackground`: Helm chart description code background color.
- `helmDescriptionPreBackground`: Helm chart description pre background color.
- `helmDescriptionPreColor`: Helm chart description pre foreground color.
## Terminal colors
- `terminalBackground`: Terminal background color.
- `terminalForeground`: Terminal foreground color.
- `terminalCursor`: Terminal cursor color.
- `terminalCursorAccent`: Terminal cursor accent color.
- `terminalSelection`: Terminal selection background color.
- `terminalBlack`: Terminal black color.
- `terminalRed`: Terminal red color.
- `terminalGreen`: Terminal green color.
- `terminalYellow`: Terminal yellow color.
- `terminalBlue`: Terminal blue color.
- `terminalMagenta`: Terminal magenta color.
- `terminalCyan`: Terminal cyan color.
- `terminalWhite`: Terminal white color.
- `terminalBrightBlack`: Terminal bright black color.
- `terminalBrightRed`: Terminal bright red color.
- `terminalBrightGreen`: Terminal bright green color.
- `terminalBrightYellow`: Terminal bright yellow color.
- `terminalBrightBlue`: Terminal bright blue color.
- `terminalBrightMagenta`: Terminal bright magenta color.
- `terminalBrightCyan`: Terminal bright cyan color.
- `terminalBrightWhite`: Terminal bright white color.
## Dialog colors
- `dialogHeaderBackground`: background color for dialog header.
- `dialogFooterBackground`: background color for dialog footer.
## Detail panel (Drawer) colors
- `drawerTitleText`: drawer title foreground color.
- `drawerSubtitleBackground`: drawer subtitle foreground color.
- `drawerItemNameColor`: foreground color for item name in drawer.
- `drawerItemValueColor`: foreground color for item value in drawer.
## Misc colors
- `logsBackground`: background color for pod logs.
- `clusterMenuBackground`: background color for cluster menu.
- `clusterMenuBorderColor`: border color for cluster menu.
- `clusterSettingsBackground`: background color for cluster settings.
- `addClusterIconColor`: add cluster button background color.
- `iconActiveColor`: active cluster icon foreground color.
- `iconActiveBackground`: active cluster icon background color.
- `filterAreaBackground`: page filter area (where selected namespaces are lister) background color.
- `chartStripesColor`: bar chart zebra stripes background color.
- `chartCapacityColor`: background color for capacity values in bar charts.
- `pieChartDefaultColor`: default background color for pie chart values.
- `selectOptionHoveredColor`: foregrond color for selected element in dropdown list.
- `lineProgressBackground`: background color for progress line.
- `radioActiveBackground`: background color for active radio buttons.
- `menuActiveBackground`: background color for active menu items.
In most cases you would only need base, text and some of the layout colors.

View File

@ -0,0 +1,258 @@
# Common Capabilities
Common Capabilities are important building blocks for your extensions. Almost all extensions use some of these functionalities. Here is how you can take advantage of them.
## Main Extension
A main extension runs in the background and, apart from app menu items, does not add content to the Lens UI. If you want to see logs from this extension you need to start Lens from the command line.
### Activate
An extension can register a custom callback that is executed when an extension is activated (started).
``` javascript
import { LensMainExtension } from "@k8slens/extensions"
export default class ExampleMainExtension extends LensMainExtension {
async onActivate() {
console.log("hello world")
}
}
```
### Deactivate
An extension can register a custom callback that is executed when an extension is deactivated (stopped).
``` javascript
import { LensMainExtension } from "@k8slens/extensions"
export default class ExampleMainExtension extends LensMainExtension {
async onDeactivate() {
console.log("bye bye")
}
}
```
### App Menus
An extension can register custom App menus that will be displayed on OS native menus.
Example:
``` typescript
import { LensMainExtension, windowManager } from "@k8slens/extensions"
export default class ExampleMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "Example item",
click() {
windowManager.navigate("https://k8slens.dev");
}
}
]
}
```
## Renderer Extension
A renderer extension runs in a browser context and it's visible directly via Lens main window. If you want to see logs from this extension you need to check them via View -> Toggle Developer Tools -> Console.
### Activate
An extension can register a custom callback that is executed when an extension is activated (started).
``` javascript
import { LensRendererExtension } from "@k8slens/extensions"
export default class ExampleExtension extends LensRendererExtension {
async onActivate() {
console.log("hello world")
}
}
```
### Deactivate
An extension can register a custom callback that is executed when an extension is deactivated (stopped).
``` javascript
import { LensRendererExtension } from "@k8slens/extensions"
export default class ExampleMainExtension extends LensRendererExtension {
async onDeactivate() {
console.log("bye bye")
}
}
```
### Global Pages
An extension can register custom global pages (views) to Lens main window. Global page is a full screen page that hides all the other content from a window.
``` typescript
import React from "react"
import { Component, LensRendererExtension } from "@k8slens/extensions"
import { ExamplePage } from "./src/example-page"
export default class ExampleRendererExtension extends LensRendererExtension {
globalPages = [
{
path: "/example-route",
hideInMenu: true,
components: {
Page: ExamplePage,
}
}
]
}
```
### App Preferences
An extension can register custom app preferences. An extension is responsible for storing a state for custom preferences.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions"
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
export default class ExampleRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "My Custom Preference",
components: {
Hint: () => <MyCustomPreferenceHint/>,
Input: () => <MyCustomPreferenceInput store={myCustomPreferencesStore}/>
}
}
]
}
```
### Cluster Pages
An extension can register custom cluster pages which are visible in a cluster menu when a cluster is opened.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./src/page"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
}
```
### Cluster Features
An extension can register installable features for a cluster. A cluster feature is visible in "Cluster Settings" page.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions"
import { MyCustomFeature } from "./src/my-custom-feature"
export default class ExampleExtension extends LensRendererExtension {
clusterFeatures = [
{
title: "My Custom Feature",
components: {
Description: () => {
return (
<span>
Just an example.
</span>
)
}
},
feature: new MyCustomFeature()
}
]
}
```
### Status Bar Items
An extension can register custom icons/texts to a status bar area.
``` typescript
import React from "react";
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
export default class ExampleExtension extends LensRendererExtension {
statusBarItems = [
{
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate("/example-page")}
>
<Component.Icon material="favorite" smallest />
</div>
)
}
]
}
```
### Kubernetes Object Menu Items
An extension can register custom menu items (actions) for specified Kubernetes kinds/apiVersions.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions";
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
export default class ExampleExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props: CustomMenuItemProps) => <CustomMenuItem {...props} />
}
}
]
}
```
### Kubernetes Object Details
An extension can register custom details (content) for specified Kubernetes kinds/apiVersions.
``` typescript
import React from "react"
import { LensRendererExtension } from "@k8slens/extensions";
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
export default class ExampleExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "CustomKind",
apiVersions: ["custom.acme.org/v1"],
components: {
Details: (props: CustomKindDetailsProps) => <CustomKindDetails {...props} />
}
}
]
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

View File

@ -0,0 +1,99 @@
# Styling an extension
Lens provides a set of global styles and UI components that can be used by any extension to preserve look and feel of the application.
## Styling approach
Lens heavily uses SCSS preprocessor with a set of predefined variables and mixins.
For layout tasks Lens is using [flex.box](https://www.npmjs.com/package/flex.box) library which provides helpful class names to specify some of the [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox) properties. For example, `div` with class names:
```
<div className="flex column align-center"></div>
```
at the end will have following css properties:
```
div {
display: flex;
flex-direction: column;
align-items: center;
}
```
However, feel free to use any styling technique or framework like [Emotion](https://github.com/emotion-js/emotion) or just plain CSS if you prefer.
## Themes
Lens using 2 built-in themes located in `src/renderer/themes` folder each for light and dark color schemes. Active theme can be changed in the `Preferences` page.
![Color Theme](images/theme-selector.png)
When Lens gets loaded it transforms selected theme `json` file into list of [CSS Custom Properties (CSS Variables)](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) which then gets injected into `:root` element so any of the down-level components can use them.
![CSS vars listed in devtools](images/css-vars-in-devtools.png)
When user changes a theme, the process is repeated, new css variables appear instead of previous ones.
If you want to follow a selected theme to keep the 'native' Lens look and feel, respecting the light/dark appearance of your extension, you can use provided variables and build-in lens components such as buttons, dropdowns, checkboxes etc.
## Injected styles
Every extention is affected by list of default global styles defined in `src/renderer/components/app.scss`. These are basic browser resets like setting `box-sizing` property for every element, default text and background colors, default font size, basic headings visualisation etc.
Extension may overwrite them if needed.
## Variables to use
### Basic styling
There is a list of CSS Variables available for extension to use. Basic variables located inside `:root` selected in `src/renderer/components/app.scss`:
```
--unit: 8px;
--padding: var(--unit);
--margin: var(--unit);
--border-radius: 3px;
--font-main: 'Roboto', 'Helvetica', 'Arial', sans-serif;
--font-monospace: Lucida Console, Monaco, Consolas, monospace;
--font-size-small: calc(1.5 * var(--unit));
--font-size: calc(1.75 * var(--unit));
--font-size-big: calc(2 * var(--unit));
--font-weight-thin: 300;
--font-weight-normal: 400;
--font-weight-bold: 500;
```
They're intended to set consistent paddings and font-sizes across components, e.g.
```
.status {
padding-left: calc(var(--padding) * 2);
font-size: var(--font-size-small);
}
```
### Themable colors
After theme file gets parsed it provides list of theme-defined colors. Most of their values are different for light and dark themes. You can use them to preserve consitent view of extension with respect of selected theme.
```
"blue": "#3d90ce",
"magenta": "#c93dce",
"golden": "#ffc63d",
"halfGray": "#87909c80",
"primary": "#3d90ce",
"textColorPrimary": "#555555",
"textColorSecondary": "#51575d",
"textColorAccent": "#333333",
"borderColor": "#c9cfd3",
"borderFaintColor": "#dfdfdf",
"mainBackground": "#f1f1f1",
"contentColor": "#ffffff",
"layoutBackground": "#e8e8e8",
"layoutTabsBackground": "#f8f8f8",
"layoutTabsActiveColor": "#333333",
"layoutTabsLineColor": "#87909c80"
...
...
```
They can be used in form of `var(--magenta)`.
A complete list of all themable colors can be found in the [color reference](../color-reference).
Colors values are located inside `src/renderer/themes/lens-dark.json` and `src/renderer/themes/lens-light.json` files.
## Using CSS Variables inside CSS-in-JS components
If a developer uses an `Emotion` (or similar) framework to work with styles inside an extension, they can use variables in the following form:
```
const Container = styled.div(() => ({
backgroundColor: 'var(--mainBackground)'
}));
```

View File

@ -0,0 +1,98 @@
# Extension Anatomy
You've now learnt how to get basic extension running. In this topic you will learn some fundamental concepts to Lens Extension development; How does it work under the hood?
`Hello World` extension does three things:
- Hooks on `onActivate()` and ouputs message into console.
- Hooks on `onDectivate()` and ouputs message into console.
- Registers `ClusterPage` so that page is visible in the sidebars of cluster dashboards.
Let's take a closer look at Hello World sample's source code and see how to achieve these things.
## Extension File Structure
```
.
├── .gitignore // Ignore build output and node_modules
├── Makefile // Config for build tasks that compiles the extension
├── README.md // Readable description of your extension's functionality
├── src
│ └── page.tsx // Extension's additional source code
├── main.ts // Source code for extension's main entrypoint
├── package.json // Extension manifest and dependencies
├── renderer.tsx // Source code for extension's renderer entrypoint
├── tsconfig.json // TypeScript configuration
├── webpack.config.js // Webpack configuration
```
Extension directory contains extension's entry files and few configuration files. Let's focus on `package.json`, `main.ts` and `renderer.tsx` which are essential to understanding the `Hello World` extension.
### Extension Manifest
Each Lens extension must have `package.json`. The `package.json` contains a mix of Node.js fields such as scripts and dependencies and Lens specific fields such as `publisher` and `contributes`. Here are some most important fields:
- `name` and `publisher`: Lens uses `@<publisher>/<name>` as a unique ID for the extension. For example, the Hello World sample has the ID `@lensapp-samples/helloworld-sample`. Lens uses the ID to uniquely identify your extension
- `main`: The extension's entry point run in `main` process.
- `renderer`: The extension's entry point run in `renderer` process.
- `engines.lens`: This specifies the minimum version of Lens API that the extension depends on.
``` javascript
{
"name": "helloworld-sample",
"publisher": "lens-samples",
"version": "0.0.1",
"description": "Lens helloworld-sample",
"license": "MIT",
"homepage": "https://github.com/lensapp/lens-extension-samples",
"engines": {
"lens": "^4.0.0"
},
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {
"react-open-doodles": "^1.0.5"
},
"devDependencies": {
"@k8slens/extensions": "^4.0.0-alpha.2",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"@types/react": "^16.9.35",
"@types/node": "^12.0.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.11"
}
}
```
## Extension Entry Files
Lens extensions can have two separate entry files. One file that is used in `main` process of Lens application and antoher that is used in `renderer` process. `main` entry file should export class that extends `LensMainExtension` and `renderer` entry file should export class that extends `LensRendererExtension`.
Both extensions classes have `onActivate` and `onDeactivate` methods. `onActivate` is executed when your extension activation happens. You may want to initialize something in your extension at this point. `onDeactivate` gives you a chance to clean up before your extension becomes deactivated. For many extensions, explicit cleanup may not be required, and you don't need to override this method. However, if an extension needs to perform an operation when Lens is shutting down or the extension is disabled or uninstalled, this is the method to do so.
`Hello world` extension does not do anything special on `main` process, so let's focus on the `renderer` side. On `renderer` entry point, `Hello world` extension defines one `Cluster Page` object that registers `/extension-example` path that renders `ExamplePage` React component. It registers also `MenuItem` component that displays `ExampleIcon` React component and "Hello World" text in the sidebar of cluster dashboards. These React components are defined in additional `./src/page.tsx` file.
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
clusterPages = [
{
path: "/extension-example",
title: "Hello World",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
}
```
`Hello World` extension uses only one capability (`Cluster Page`) of Lens extensions. The [Extension Capabilities Overview](/extensions/capabilities/) topic helps you find the right capabilities you can use with your own extension.

View File

@ -0,0 +1,20 @@
# Extension Development Overview
This is a general overview to how the development of an extension will procede. For building extensions there will be a few things that you should have installed, and some that might help.
### Required:
- [Node.js](https://www.nodejs.org/en/)
- [Git](https://www.git-scm.com/)
- Some sort of text editor, we recommend [VSCode](https://code.visualstudio.com/)
- We use [Webpack](https://www.webpack.js.org/) for compilation. All extension need to be at least compatable with a webpack system.
### Recommened:
-
All *Lens* extensions are javascript packages. We recommend that you program in [Typescript](https://www.typescriptlang.org/) because it catches quite a few easily to make errors around passing data around in javascript.
*Lens* is a standard [Electron](https://www.electronjs.org/) application which both main and renderer processes. An extension is made up of two parts, one for each of *Lens*'s core processes. When an extension is loaded each part is loaded and then notified that it has been loaded. From there the extension can start doing is work.
*Lens* uses [React](https://www.reactjs.org/) as it UI framework and even provides some of our own components for reuse by extensions. An extension is resonsible for the lifetime of any resources it spins up. If an extension's main part starts new processes they all must be stopped and cleaned up when the extension is deactivated or unloaded.
See [Your First Extension](your-first-extension.md) to get started.

View File

@ -0,0 +1,18 @@
# Wrapping Up
In the [Your First Extension](your-first-extension.md) topic, you learned how to create and run an extension. In the [Extension Anatomy](anatomy.md) topic, you learned fundamental concepts to Lens extension development. However, this is just a small glimpse of what can be created with Lens Extensions. Below are some suggested routes for furthering your Lens extension development skills.
## Extension Capabilities
In this section, we split the Lens extension points into a few categories, each with short descriptions as to what your extension could achieve. Validate that your extension idea is achievable by reading the [Extension Capabilities](../capabilities/overview.md) section for new extension ideas.
## Guides & Samples
We have a great collection of sample extensions that you can adapt from, and some of them include a detailed guide that explains the source code. You can find all Samples & Guides in the [lens-extension-samples](https://github.com/lensapp/lens-extension-samples) repository.
## Testing and Publishing
This section includes topics that help you develop high-quality Lens extensions. For example, you can learn
* How to add [integration tests](../testing-and-publishing/testing.md) for your extension
* How to [publish your extension](../testing-and-publishing/publishing.md)

View File

@ -0,0 +1,63 @@
# Your First Extension
In this topic, we'll teach you the fundamental concepts for building extensions.
## Installing and Building the extension
Simple Lens extension that adds "Hello World" page to a cluster menu.
### Linux
First you will need to clone the [Lens Extension samples](https://github.com/lensapp/lens-extension-samples) repository to your local machine:
```sh
git clone https://github.com/lensapp/lens-extension-samples.git
```
Next you need to create a symlink from the directory that Lens will monitor for user installed extensions to the sample extension, in this case **helloworld-sample**:
```sh
mkdir -p ~/.k8slens/extensions
cd ~/.k8slens/extensions
ln -s <lens-extension-samples directory>/helloworld-sample helloworld-sample
```
To build the extension you can use `make` or run the `npm` commands manually:
```sh
cd <lens-extension-samples directory>/helloworld-sample
make build
```
OR
```sh
cd <lens-extension-samples directory>/helloworld-sample
npm install
npm run build
```
If you want to watch for any source code changes and automatically rebuild the extension you can use:
```sh
cd <lens-extension-samples directory>/helloworld-sample
npm run dev
```
Finally, if you already have Lens open you will need to quit and restart Lens for the extension to be loaded. After this initial restart you can reload Lens and it will pick up any new builds of the extension. Within Lens connect to an existing cluster or [create a new one](../../clusters/adding-clusters.md). You should see then see the "Hello World" page in the Lens sidebar cluster menu.
## Developing the extension
Let's make a change to the message that our helloworld-sample extension displays:
* Navigate to `<lens-extension-samples directory>/helloworld-sample`.
* Change the message from HelloWorld! to **Hello Lens Extensions** in `page.tsx`.
* Rebuild the extension or, if you used `npm run dev`, the extension should automatically rebuild.
* Reload the Lens window and click on the Hello World page.
* You should see the updated message showing up.
## Next steps
In the next topic, [Extension Anatomy](anatomy.md), we'll take a closer look at the source code of the Hello World sample and explain key concepts.
You can find the source code of this tutorial at: [lensapp/lens-extension-samples](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample). The [Extension Guides](../guides/overview.md) topic contains other samples, each illustrating a different Lens Extension API.

View File

View File

@ -0,0 +1 @@
# Renderer Extension

View File

@ -0,0 +1,32 @@
# Testing Extensions
## Console.log
`console.log()` might be handy for extension developers to prints out info/errors from extensions. To use `console.log`, note that Lens is based on Electron. Electron has two types of processes: [Main and Renderer](https://www.electronjs.org/docs/tutorial/quick-start#main-and-renderer-processes).
### Renderer process logs
`console.log()` in Renderer process is printed in the Console in Developer Tools (View > Toggle Developer Tools).
### Main process logs
To view the logs from the main process is a bit trickier, since you cannot open developer tools for them. On MacOSX, one way is to run Lens from the terminal.
```bash
/Applications/Lens.app/Contents/MacOS/Lens
```
You can alos use [Console.app](https://support.apple.com/en-gb/guide/console/welcome/mac) to view logs from Lens.
On linux, you can get PID of Lens first
```bash
ps aux | grep Lens | grep -v grep
```
And get logs by the PID
```bash
tail -f /proc/[pid]/fd/1 # stdout (console.log)
tail -f /proc/[pid]/fd/2 # stdout (console.error)
```

View File

@ -0,0 +1,3 @@
# Using Extensions
TBD

1
docs/faq/README.md Normal file
View File

@ -0,0 +1 @@
TBD

View File

@ -0,0 +1,55 @@
# Getting Started
Lens is lightweight and simple to install. You'll be up and running in just a few minutes.
## System requirements
Review the [System Requirements](/supporting/requirements/) to check if your computer configuration is supported.
## macOS
1. [Download Lens](https://github.com/lensapp/lens/releases) for macOS.
2. Open the browser's download list and locate the downloaded archive.
3. Select the 'magnifying glass' icon to open the archive in Finder.
4. Double-click `Lens-{version}.dmg` and drag `Lens.app` to the `Applications` folder, making it available in the macOS Launchpad.
5. Add Lens to your Dock by right-clicking on the icon to bring up the context menu and choosing **Options**, **Keep in Dock**.
## Windows
1. Download the [Lens installer](https://github.com/lensapp/lens/releases) for Windows.
2. Once it is downloaded, run the installer `Lens-Setup-{version}.exe`. This will only take a minute.
3. By default, Lens is installed under `C:\users\{username}\AppData\Local\Programs\Lens`.
## Linux
See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options.
### Snap
Lens is officially distributed as a Snap package in the [Snap Store](https://snapcraft.io/store):
[![Get it from the Snap Store](images/snap-store.png)](https://snapcraft.io/kontena-lens)
You can install it by running:
```bash
sudo snap install kontena-lens --classic
```
## Update cadence
Lens releases a new version each month with new features and important bug fixes. Lens supports auto updating and you will be prompted to install the new release when it becomes available!
To stay current with the Lens features, you can review the [release notes](https://github.com/lensapp/lens/releases).
## Next Steps
- [Add clusters](../clusters/adding-clusters.md)
- [Watch introductory videos](./introductory-videos.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,36 @@
# Introductory Videos
Continue your Lens journey with this set of introductory videos! These videos are meant to quickly familiarize you with Lens' various powerful features.
<ul class="video-list">
<li class="video">
<a target="_blank" href="https://youtu.be/mc-BzPDKfkQ">
<img src="https://img.youtube.com/vi/mc-BzPDKfkQ/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Getting started</h3>
<p class="description">Get Lens Kubernetes IDE Running in 5 Minutes</p>
<span class="duration"><span class="sr-only">Duration </span>35<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
<li class="video">
<a target="_blank" href="https://youtu.be/epw_MjxjMYI">
<img src="https://img.youtube.com/vi/epw_MjxjMYI/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Introducing Lens</h3>
<p class="description">Lens Kubernetes IDE overview</p>
<span class="duration"><span class="sr-only">Duration </span>2<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
<li class="video">
<a target="_blank" href="https://youtu.be/fqneoWCgJdw">
<img src="https://img.youtube.com/vi/fqneoWCgJdw/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Demo of Mirantis Lens</h3>
<p class="description">The Best IDE For Kubernetes</p>
<span class="duration"><span class="sr-only">Duration </span>10<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
</ul>

View File

@ -0,0 +1,28 @@
# Preferences
## Color themes
The Color Themes option in Lens preferences lets you set the colors in the Lens user interface to suit your liking.
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac).
2. Select your preferred theme from the **Color Theme** dropdown.
![Color Theme](images/color-theme.png)
## Telemetry & usage tracking
Lens collects telemetry data, which is used to help us understand how to improve the product. For example, this usage data helps us to debug issues and to prioritize new features. While we appreciate the insights this data provides, we also know that not everyone wants to send usage data. Please see our [privacy statement](https://www.mirantis.com/company/privacy-policy/) to learn more.
### Disable telemetry reporting
If you don't wish to send usage data to Mirantis, you can disable the "Telemetry & Usage Tracking" in the Lens preferences.
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac).
2. Scroll down to **Telemetry & Usage Tracking**
3. Uncheck **Allow telemetry & usage tracking**.
This will silence all telemetry events from Lens going forward. Telemetry information may have been collected and sent up until the point when you disable this setting.
![Disable Telemetry & Usage Tracking](images/disabled-telemetry-usage-tracking.png)

3
docs/helm/README.md Normal file
View File

@ -0,0 +1,3 @@
# Using Helm Charts
TBD

BIN
docs/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#3D90CE;}
.st2{fill:#FFFFFF;}
</style>
<rect y="0" class="st0" width="512" height="512"/>
<rect x="16" y="16" class="st1" width="480" height="480"/>
<g>
<path class="st2" d="M242.6,426h130.9l-30.2-128.7L242.6,426z"/>
<path class="st2" d="M86,352.5V426h137.6l57.5-73.5H86z"/>
<path class="st2" d="M273.1,167L426,241.4v-148L273.1,167z"/>
<path class="st2" d="M388.9,426H426V258l-86.4-42.1L388.9,426z"/>
<path class="st2" d="M406.9,86H216.4l-23,102.7L406.9,86z"/>
<path class="st2" d="M86,195.1v142.5h113.3L86,195.1z"/>
<path class="st2" d="M201.1,86H86v85.1l75,94.3L201.1,86z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 999 B

1
docs/img/play.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 22 22"><path fill="#fff" d="M9.35 15.95V6.6L15.4 11zM11 1C5.477 1 1 5.477 1 11c0 5.522 4.477 10 10 10s10-4.478 10-10c0-5.523-4.477-10-10-10zm0 18.5a8.5 8.5 0 1 1 .001-17.001A8.5 8.5 0 0 1 11 19.5z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,90 @@
:root {
--md-primary-fg-color: #3d90ce;
--md-accent-fg-color: #3d90ce;
}
:root > * {
/* Footer */
--md-footer-bg-color: #3d90ce;
}
.md-version__list {
overflow: auto;
}
ul.video-list {
counter-reset: section;
list-style: none;
padding-left: 0;
position:relative
}
ul.video-list .video:not(:last-of-type) a {
border-bottom:2px solid #e6e6e6
}
ul.video-list a {
position: relative;
padding: 2rem 2rem 2rem 4.2rem;
display: block;
box-sizing:border-box
}
ul.video-list a .info {
padding-top:0.25rem
}
ul.video-list a .info::before {
counter-increment: section;
content: counter(section);
position: absolute;
left: 1.5rem;
color:black
}
ul.video-list a .info > p, ul.video-list a .info > span {
color:black
}
ul.video-list a .info .title {
margin-top: 0;
margin-bottom:0.7rem
}
ul.video-list a .info .description {
margin-bottom: 1rem;
}
ul.video-list a .info .duration, ul.video-list a .info .duration span {
color: #6e6e6e;
}
ul.video-list a:hover, ul.video-list a:focus {
text-decoration:none
}
ul.video-list a:hover {
background:#f2f2f2
}
ul.video-list a:hover::after {
content: '';
display: block;
width: 6rem;
height: 6rem;
position: absolute;
background: url("/img/play.svg");
background-size: 6rem;
top: 3rem;
left:8.6rem
}
ul.video-list .thumb {
max-height: 8.2rem;
padding-right: 2rem;
position: relative;
float:left
}

View File

@ -0,0 +1,26 @@
# Requirements for Lens
## Hardware
Lens is a small download (< 300 MB) and has a disk footprint of 600 MB. Lens is lightweight and should easily run on today's hardware.
We recommend:
* 2 GHz or faster processor
* 1 GB of RAM
## Platforms
Lens has been tested on the following platforms:
* OS X
* Windows
* Linux
### Additional Windows requirements
...
### Additional Linux requirements
...

View File

@ -47,17 +47,23 @@ describe("Lens integration tests", () => {
await clickWhatsNew(app)
})
// Todo figure out how to access main menu to get these to work
it.skip('shows "add cluster"', async () => {
await app.client.keys(['Shift', 'Meta', 'A'])
it('shows "add cluster"', async () => {
await app.electron.ipcRenderer.send('test-menu-item-click', "File", "Add Cluster")
await app.client.waitUntilTextExists("h2", "Add Cluster")
await app.client.keys(['Shift', 'Meta'])
})
it.skip('shows "preferences"', async () => {
await app.client.keys(['Meta', ','])
await app.client.waitUntilTextExists("h2", "Preferences")
await app.client.keys('Meta')
describe("preferences page", () => {
it('shows "preferences"', async () => {
let appName: string = process.platform === "darwin" ? "Lens" : "File"
await app.electron.ipcRenderer.send('test-menu-item-click', appName, "Preferences")
await app.client.waitUntilTextExists("h2", "Preferences")
})
it('ensures helm repos', async () => {
await app.client.waitUntilTextExists("div.repos #message-stable", "stable") // wait for the helm-cli to fetch the stable repo
await app.client.click("#HelmRepoSelect") // click the repo select to activate the drop-down
await app.client.waitUntilTextExists("div.Select__option", "") // wait for at least one option to appear (any text)
})
})
it.skip('quits Lens"', async () => {
@ -410,6 +416,40 @@ describe("Lens integration tests", () => {
})
})
describe("viewing pod logs", () => {
beforeEach(appStartAddCluster, 40000)
afterEach(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})
it(`shows a logs for a pod`, async () => {
expect(clusterAdded).toBe(true)
// Go to Pods page
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
await app.client.click('a[href^="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
// Open logs tab in dock
await app.client.click(".list .TableRow:first-child")
await app.client.waitForVisible(".Drawer")
await app.client.click(".drawer-title .Menu li:nth-child(2)")
// Check if controls are available
await app.client.waitForVisible(".PodLogs .VirtualList")
await app.client.waitForVisible(".PodLogControls")
await app.client.waitForVisible(".PodLogControls .SearchInput")
await app.client.waitForVisible(".PodLogControls .SearchInput input")
// Search for semicolon
await app.client.keys(":")
await app.client.waitForVisible(".PodLogs .list span.active")
// Click through controls
await app.client.click(".PodLogControls .timestamps-icon")
await app.client.click(".PodLogControls .undo-icon")
})
})
describe("cluster operations", () => {
beforeEach(appStartAddCluster, 40000)

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

100
mkdocs.yml Normal file
View File

@ -0,0 +1,100 @@
site_name: Lens Documentation
site_description: Documentation for Lens IDE. The only system youll ever need to take control of your Kubernetes clusters. It's open source and free. Download it today!
site_author: Mirantis, Inc.
site_url: https://docs.k8slens.dev
docs_dir: docs/
repo_name: GitHub
repo_url: https://github.com/lensapp/lens
copyright: Copyright &copy; 2020 <a href="https://mirantis.com/">Mirantis Inc.</a> - All rights reserved.
edit_uri: ""
nav:
- Overview: README.md
- Getting Started: getting-started/README.md
- Managing Clusters:
- Adding clusters: clusters/adding-clusters.md
- Removing cluster: clusters/removing-clusters.md
- Settings: clusters/settings.md
- Using Helm Charts: helm/README.md
- Using Extensions: extensions/usage/README.md
- Preferences: getting-started/preferences.md
- Extension Development:
- Overview: extensions/README.md
- Get Started:
- Your First Extension: extensions/get-started/your-first-extension.md
- Extension Anatomy: extensions/get-started/anatomy.md
- Wrapping Up: extensions/get-started/wrapping-up.md
- Extension Capabilities:
- Overview: extensions/capabilities/README.md
- Common Capabilities: extensions/capabilities/common-capabilities.md
- Styling: extensions/capabilities/styling.md
- Color reference: extensions/capabilities/color-reference.md
- Extension Guides:
- Overview: extensions/guides/README.md
- Renderer Extension: extensions/guides/renderer-extension.md
- Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
- Bundling Extensions: extensions/testing-and-publishing/bundling.md
- API Reference: extensions/api/README.md
- Contributing:
- Overview: contributing/README.md
- Development: contributing/development.md
- Documentation: contributing/documentation.md
- Maintainers: contributing/maintainers.md
- Promotion: contributing/promotion.md
- FAQ: faq/README.md
theme:
name: 'material'
highlightjs: true
language: 'en'
custom_dir: docs/custom_theme
favicon: img/favicon.ico
logo: img/lens-logo-icon.svg
palette:
- scheme: default
toggle:
icon: material/toggle-switch
name: Switch to light mode
- scheme: slate
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
features:
- navigation.instant
- toc.autohide
- search.suggest
- search.highlight
extra_css:
- stylesheets/extra.css
plugins:
- search
- git-revision-date-localized
markdown_extensions:
- pymdownx.highlight: {}
- pymdownx.superfences: {}
- pymdownx.inlinehilite: {}
- toc:
permalink: "#"
toc_depth: 3
extra:
generator: false
social:
- icon: fontawesome/brands/github
link: https://github.com/lensapp/lens
name: Lens on GitHub
- icon: fontawesome/brands/twitter
link: https://twitter.com/k8slens
name: Lens on Twitter
- icon: fontawesome/brands/slack
link: http://k8slens.slack.com/
name: Lens on Slack
- icon: fontawesome/solid/link
link: https://k8slens.dev/
name: Lens Website
version:
method: mike

35
mkdocs/Dockerfile Normal file
View File

@ -0,0 +1,35 @@
ARG PYTHON_VERSION=3.8.1-alpine3.11
FROM python:${PYTHON_VERSION} as builder
ENV PYTHONUNBUFFERED 1
# Set build directory
WORKDIR /wheels
# Copy files necessary
COPY ./requirements.txt .
# Perform build and cleanup artifacts
RUN \
apk add --no-cache \
git \
git-fast-import \
&& apk add --no-cache --virtual .build gcc musl-dev \
&& python -m pip install --upgrade pip \
&& pip install -r requirements.txt \
&& apk del .build gcc musl-dev \
&& rm -rf /usr/local/lib/python3.8/site-packages/mkdocs/themes/*/* \
&& rm -rf /tmp/*
# Set final MkDocs working directory
WORKDIR /docs
# Expose MkDocs development server port
EXPOSE 8000
# Start development server by default
ENTRYPOINT ["mkdocs"]
CMD ["serve", "--dev-addr=0.0.0.0:8000"]

8
mkdocs/requirements.txt Normal file
View File

@ -0,0 +1,8 @@
# Direct dependencies
mkdocs>=1.1
Pygments>=2.4
markdown>=3.2
pymdown-extensions>=7.0
mkdocs-material-extensions>=1.0
mkdocs-git-revision-date-localized-plugin>=0.7.3
mkdocs-material>=6.1.0

View File

@ -15,7 +15,7 @@
"dev-build": "concurrently yarn:compile:*",
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
"dev:main": "yarn compile:main --watch",
"dev:renderer": "yarn compile:renderer --watch",
"dev:renderer": "yarn webpack-dev-server --config webpack.renderer.ts",
"dev:extension-types": "yarn compile:extension-types --watch",
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "webpack --config webpack.main.ts",
@ -37,7 +37,8 @@
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/",
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest"
},
"config": {
"bundledKubectlVersion": "1.17.11",
@ -268,6 +269,7 @@
"@lingui/macro": "^3.0.0-13",
"@lingui/react": "^3.0.0-13",
"@material-ui/core": "^4.10.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@rollup/plugin-json": "^4.1.0",
"@testing-library/jest-dom": "^5.11.5",
"@testing-library/react": "^11.1.0",
@ -279,6 +281,7 @@
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/hapi": "^18.0.3",
"@types/hard-source-webpack-plugin": "^1.0.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.3",
"@types/http-proxy": "^1.17.4",
@ -315,6 +318,7 @@
"@types/uuid": "^8.0.0",
"@types/webdriverio": "^4.13.0",
"@types/webpack": "^4.41.17",
"@types/webpack-dev-server": "^3.11.1",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^4.0.0",
@ -339,6 +343,7 @@
"file-loader": "^6.0.0",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^5.0.0",
"hard-source-webpack-plugin": "^0.13.1",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^4.3.0",
"identity-obj-proxy": "^3.0.0",
@ -360,6 +365,7 @@
"react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-refresh": "^0.9.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0",
@ -376,11 +382,13 @@
"ts-jest": "^26.1.0",
"ts-loader": "^7.0.5",
"ts-node": "^8.10.2",
"type-fest": "^0.18.0",
"typeface-roboto": "^0.0.75",
"typescript": "^4.0.2",
"url-loader": "^4.1.0",
"webpack": "^4.43.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
"webpack-node-externals": "^1.7.2",
"xterm": "^4.6.0",
"xterm-addon-fit": "^0.4.0"

View File

@ -0,0 +1,80 @@
/**
* @jest-environment jsdom
*/
import { SearchStore } from "../search-store"
let searchStore: SearchStore = null;
const logs = [
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
"1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407."
]
describe("search store tests", () => {
beforeEach(async () => {
searchStore = new SearchStore();
})
it("does nothing with empty search query", () => {
searchStore.onSearch([], "");
expect(searchStore.occurrences).toEqual([]);
})
it("doesn't break if no text provided", () => {
searchStore.onSearch(null, "replica");
expect(searchStore.occurrences).toEqual([]);
searchStore.onSearch([], "replica");
expect(searchStore.occurrences).toEqual([]);
})
it("find 3 occurences across 3 lines", () => {
searchStore.onSearch(logs, "172");
expect(searchStore.occurrences).toEqual([0, 1, 2]);
})
it("find occurences within 1 line (case-insensitive)", () => {
searchStore.onSearch(logs, "Starting");
expect(searchStore.occurrences).toEqual([2, 2]);
})
it("sets overlay index equal to first occurence", () => {
searchStore.onSearch(logs, "Replica");
expect(searchStore.activeOverlayIndex).toBe(0);
})
it("set overlay index to next occurence", () => {
searchStore.onSearch(logs, "172");
searchStore.setNextOverlayActive();
expect(searchStore.activeOverlayIndex).toBe(1);
})
it("sets overlay to last occurence", () => {
searchStore.onSearch(logs, "172");
searchStore.setPrevOverlayActive();
expect(searchStore.activeOverlayIndex).toBe(2);
})
it("gets line index where overlay is located", () => {
searchStore.onSearch(logs, "synchronization");
expect(searchStore.activeOverlayLine).toBe(1);
})
it("escapes string for using in regex", () => {
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
})
it("gets active find number", () => {
searchStore.onSearch(logs, "172");
searchStore.setNextOverlayActive();
expect(searchStore.activeFind).toBe(2);
})
it("gets total finds number", () => {
searchStore.onSearch(logs, "Starting");
expect(searchStore.totalFinds).toBe(2);
})
})

View File

@ -39,6 +39,7 @@ export interface ClusterModel {
preferences?: ClusterPreferences;
metadata?: ClusterMetadata;
ownerRef?: string;
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
@ -179,8 +180,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
@action
addCluster(model: ClusterModel | Cluster ): Cluster {
appEventBus.emit({name: "cluster", action: "add"})
addCluster(model: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" })
let cluster = model as Cluster;
if (!(model instanceof Cluster)) {
cluster = new Cluster(model)
@ -195,7 +196,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
async removeById(clusterId: ClusterId) {
appEventBus.emit({name: "cluster", action: "remove"})
appEventBus.emit({ name: "cluster", action: "remove" })
const cluster = this.getById(clusterId);
if (cluster) {
this.clusters.delete(clusterId);

126
src/common/search-store.ts Normal file
View File

@ -0,0 +1,126 @@
import { action, computed, observable } from "mobx";
import { autobind } from "../renderer/utils";
export class SearchStore {
@observable searchQuery = ""; // Text in the search input
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
/**
* Sets default activeOverlayIndex
* @param text An array of any textual data (logs, for example)
* @param query Search query from input
*/
@action
onSearch(text: string[], query = this.searchQuery) {
this.searchQuery = query;
if (!query) {
this.reset();
return;
}
this.occurrences = this.findOccurences(text, query);
if (!this.occurrences.length) return;
// If new highlighted keyword in exact same place as previous one, then no changing in active overlay
if (this.occurrences[this.activeOverlayIndex] !== undefined) return;
this.activeOverlayIndex = this.getNextOverlay(true);
}
/**
* Does searching within text array, create a list of search keyword occurences.
* Each keyword "occurency" is saved as index of the the line where keyword founded
* @param text An array of any textual data (logs, for example)
* @param query Search query from input
* @returns {Array} Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
*/
findOccurences(text: string[], query: string) {
if (!text) return [];
const occurences: number[] = [];
text.forEach((line, index) => {
const regex = new RegExp(this.escapeRegex(query), "gi");
const matches = [...line.matchAll(regex)];
matches.forEach(() => occurences.push(index));
});
return occurences;
}
/**
* Getting next overlay index within the occurences array
* @param loopOver Allows to jump from last element to first
* @returns {number} next overlay index
*/
getNextOverlay(loopOver = false) {
const next = this.activeOverlayIndex + 1;
if (next > this.occurrences.length - 1) {
return loopOver ? 0 : this.activeOverlayIndex;
}
return next;
}
/**
* Getting previous overlay index within the occurences array of occurences
* @param loopOver Allows to jump from first element to last one
* @returns {number} prev overlay index
*/
getPrevOverlay(loopOver = false) {
const prev = this.activeOverlayIndex - 1;
if (prev < 0) {
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
}
return prev;
}
@autobind()
setNextOverlayActive() {
this.activeOverlayIndex = this.getNextOverlay(true);
}
@autobind()
setPrevOverlayActive() {
this.activeOverlayIndex = this.getPrevOverlay(true);
}
/**
* Gets line index of where active overlay is located
* @returns {number} A line index within the text/logs array
*/
@computed get activeOverlayLine(): number {
return this.occurrences[this.activeOverlayIndex];
}
@computed get activeFind(): number {
return this.activeOverlayIndex + 1;
}
@computed get totalFinds(): number {
return this.occurrences.length;
}
/**
* Checks if overlay is active (to highlight it with orange background usually)
* @param line Index of the line where overlay is located
* @param occurence Number of the overlay within one line
*/
@autobind()
isActiveOverlay(line: number, occurence: number) {
const firstLineIndex = this.occurrences.findIndex(item => item === line);
return firstLineIndex + occurence === this.activeOverlayIndex;
}
/**
* An utility methods escaping user string to safely pass it into new Regex(variable)
* @param value Unescaped string
*/
escapeRegex(value: string) {
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
}
@action
reset() {
this.searchQuery = "";
this.activeOverlayIndex = -1;
this.occurrences = [];
}
}
export const searchStore = new SearchStore;

View File

@ -31,7 +31,7 @@ export interface UserPreferences {
}
export class UserStore extends BaseStore<UserStoreModel> {
static readonly defaultTheme: ThemeId = "kontena-dark"
static readonly defaultTheme: ThemeId = "lens-dark"
private constructor() {
super({

View File

@ -22,6 +22,7 @@ export const mainDir = path.join(contextDir, "src/main");
export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
export const webpackDevServerPort = 9009
// Special runtime paths
defineGlobal("__static", {

View File

@ -4,11 +4,11 @@ import type { LensRendererExtension } from "./lens-renderer-extension"
import type { InstalledExtension } from "./extension-manager";
import path from "path"
import { broadcastIpc } from "../common/ipc"
import { computed, observable, reaction, when } from "mobx"
import { action, computed, observable, reaction, toJS, when } from "mobx"
import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron"
import * as registries from "./registries";
import { kubeObjectStatusRegistry } from "./registries";
import { extensionsStore } from "./extensions-store";
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
@ -16,33 +16,42 @@ export function extensionPackagesRoot() {
}
export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>();
@observable isLoaded = false;
protected extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
protected instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
whenLoaded = when(() => this.isLoaded);
constructor() {
if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
ipcRenderer.on("extensions:loaded", (event, extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true;
extensions.forEach((ext) => {
if (!this.extensions.has(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext)
extensions.forEach(([extId, ext]) => {
if (!this.extensions.has(extId)) {
this.extensions.set(extId, ext)
}
})
});
}
extensionsStore.manageState(this);
}
@computed get userExtensions(): LensExtension[] {
return [...this.instances.values()].filter(ext => !ext.isBundled)
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
const extensions = this.extensions.toJS();
extensions.forEach((ext, extId) => {
if (ext.isBundled) {
extensions.delete(extId);
}
})
return extensions;
}
async init() {
const { extensionManager } = await import("./extension-manager");
const installedExtensions = await extensionManager.load();
this.extensions.replace(installedExtensions);
@action
async init(extensions: Map<LensExtensionId, InstalledExtension>) {
this.extensions.replace(extensions);
this.isLoaded = true;
this.loadOnMain();
this.broadcastExtensions();
}
loadOnMain() {
@ -74,21 +83,26 @@ export class ExtensionLoader {
}
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath)
if (!instance) {
const extensionModule = this.requireExtension(ext)
if (!extensionModule) {
continue
}
return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, ext] of installedExtensions) {
let instance = this.instances.get(extId);
if (ext.isEnabled && !instance) {
try {
const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext)
if (!LensExtensionClass) continue;
instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance));
this.instances.set(ext.manifestPath, instance);
instance.enable();
this.instances.set(extId, instance);
} catch (err) {
logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err })
}
} else if (!ext.isEnabled && instance) {
try {
instance.disable();
this.instances.delete(extId);
} catch (err) {
logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err })
}
}
}
@ -106,7 +120,7 @@ export class ExtensionLoader {
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main))
}
if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint)
return __non_webpack_require__(extEntrypoint).default;
}
} catch (err) {
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
@ -114,6 +128,17 @@ export class ExtensionLoader {
}
}
getExtension(extId: LensExtensionId): InstalledExtension {
return this.extensions.get(extId);
}
toJSON(): Map<LensExtensionId, InstalledExtension> {
return toJS(this.extensions, {
exportMapsAsObjects: false,
recurseEverything: true,
})
}
async broadcastExtensions(frameId?: number) {
await when(() => this.isLoaded);
broadcastIpc({
@ -121,7 +146,7 @@ export class ExtensionLoader {
frameId: frameId,
frameOnly: !!frameId,
args: [
Array.from(this.extensions.toJS().values())
Array.from(this.toJSON()),
],
})
}

View File

@ -8,9 +8,10 @@ import { extensionPackagesRoot } from "./extension-loader"
import { getBundledExtensions } from "../common/utils/app-version"
export interface InstalledExtension {
manifest: LensExtensionManifest;
manifestPath: string;
isBundled?: boolean; // defined in package.json
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
type Dependencies = {
@ -77,7 +78,7 @@ export class ExtensionManager {
return await this.loadExtensions();
}
protected async getByManifest(manifestPath: string): Promise<InstalledExtension> {
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest;
try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
@ -88,6 +89,8 @@ export class ExtensionManager {
return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson,
isBundled: isBundled,
isEnabled: isBundled,
}
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
@ -129,9 +132,8 @@ export class ExtensionManager {
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath).catch(() => null)
const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null)
if (ext) {
ext.isBundled = true;
extensions.push(ext)
}
}

View File

@ -0,0 +1,77 @@
import type { LensExtensionId } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader";
import { BaseStore } from "../common/base-store"
import { action, observable, reaction, toJS } from "mobx";
export interface LensExtensionsStoreModel {
extensions: Record<LensExtensionId, LensExtensionState>;
}
export interface LensExtensionState {
enabled?: boolean;
}
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
constructor() {
super({
configName: "lens-extensions",
});
}
protected state = observable.map<LensExtensionId, LensExtensionState>();
protected getState(extensionLoader: ExtensionLoader) {
const state: Record<LensExtensionId, LensExtensionState> = {};
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
state[extId] = {
enabled: ext.isEnabled,
}
return state;
}, state)
}
async manageState(extensionLoader: ExtensionLoader) {
await extensionLoader.whenLoaded;
await this.whenLoaded;
// activate user-extensions when state is ready
extensionLoader.userExtensions.forEach((ext, extId) => {
ext.isEnabled = this.isEnabled(extId);
});
// apply state on changes from store
reaction(() => this.state.toJS(), extensionsState => {
extensionsState.forEach((state, extId) => {
const ext = extensionLoader.getExtension(extId);
if (ext && !ext.isBundled) {
ext.isEnabled = state.enabled;
}
})
})
// save state on change `extension.isEnabled`
reaction(() => this.getState(extensionLoader), extensionsState => {
this.state.merge(extensionsState)
})
}
isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId);
return !state /* enabled by default */ || state.enabled;
}
@action
protected fromStore({ extensions }: LensExtensionsStoreModel) {
this.state.merge(extensions);
}
toJSON(): LensExtensionsStoreModel {
return toJS({
extensions: this.state.toJSON(),
}, {
recurseEverything: true
})
}
}
export const extensionsStore = new ExtensionsStore();

View File

@ -1,7 +1,6 @@
import type { InstalledExtension } from "./extension-manager";
import { action, reaction } from "mobx";
import { action, observable, reaction } from "mobx";
import logger from "../main/logger";
import { ExtensionStore } from "./extension-store";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -14,35 +13,17 @@ export interface LensExtensionManifest {
renderer?: string; // path to %ext/dist/renderer.js
}
export interface LensExtensionStoreModel {
isEnabled: boolean;
}
export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = any> {
protected store: S;
export class LensExtension {
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@observable private isEnabled = false;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.manifest = manifest
this.manifestPath = manifestPath
this.isBundled = !!isBundled
this.init();
}
protected async init(store: S = createBaseStore().getInstance()) {
this.store = store;
await this.store.loadExtension(this);
reaction(() => this.store.data.isEnabled, (isEnabled = true) => {
this.toggle(isEnabled); // handle activation & deactivation
}, {
fireImmediately: true
});
}
get isEnabled() {
return !!this.store.data.isEnabled;
}
get id(): LensExtensionId {
@ -64,7 +45,7 @@ export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = a
@action
async enable() {
if (this.isEnabled) return;
this.store.data.isEnabled = true;
this.isEnabled = true;
this.onActivate();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@ -72,7 +53,7 @@ export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = a
@action
async disable() {
if (!this.isEnabled) return;
this.store.data.isEnabled = false;
this.isEnabled = false;
this.onDeactivate();
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
}
@ -114,13 +95,3 @@ export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = a
// mock
}
}
function createBaseStore() {
return class extends ExtensionStore<LensExtensionStoreModel> {
constructor() {
super({
configName: "state"
});
}
}
}

View File

@ -1,23 +1,38 @@
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
// Common UI components
export * from "../../renderer/components/icon"
export * from "../../renderer/components/checkbox"
export * from "../../renderer/components/tooltip"
export * from "../../renderer/components/button"
export * from "../../renderer/components/tabs"
export * from "../../renderer/components/badge"
// layouts
export * from "../../renderer/components/layout/page-layout"
export * from "../../renderer/components/layout/wizard-layout"
export * from "../../renderer/components/layout/tab-layout"
// form-controls
export * from "../../renderer/components/button"
export * from "../../renderer/components/checkbox"
export * from "../../renderer/components/radio"
export * from "../../renderer/components/select"
export * from "../../renderer/components/slider"
export * from "../../renderer/components/input/input"
// other components
export * from "../../renderer/components/icon"
export * from "../../renderer/components/tooltip"
export * from "../../renderer/components/tabs"
export * from "../../renderer/components/table"
export * from "../../renderer/components/badge"
export * from "../../renderer/components/drawer"
export * from "../../renderer/components/dialog"
export * from "../../renderer/components/confirm-dialog";
export * from "../../renderer/components/line-progress"
export * from "../../renderer/components/menu"
export * from "../../renderer/components/notifications"
export * from "../../renderer/components/spinner"
export * from "../../renderer/components/stepper"
// kube helpers
export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object"
export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta"
export { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../renderer/components/kube-object/kube-object-list-layout";
export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details"
export * from "../../renderer/components/kube-object"
export * from "../../renderer/components/+events/kube-event-details"
// specific exports
export { ConfirmDialog } from "../../renderer/components/confirm-dialog";
export { MenuItem, SubMenu } from "../../renderer/components/menu";
export { StatusBrick } from "../../renderer/components/status-brick";
export * from "../../renderer/components/status-brick";
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";

View File

@ -42,3 +42,4 @@ export enum KubeObjectStatusLevel {
WARNING = 2,
CRITICAL = 3
}
export { CustomResourceDefinition, crdApi } from "../../renderer/api/endpoints";

View File

@ -80,13 +80,14 @@ export class Cluster implements ClusterModel, ClusterState {
@observable metadata: ClusterMetadata = {};
@observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = [];
@observable accessibleNamespaces: string[] = [];
@computed get available() {
return this.accessible && !this.disconnected;
}
get version(): string {
return String(this.metadata?.version) || ""
return String(this.metadata?.version) || ""
}
constructor(model: ClusterModel) {
@ -149,7 +150,7 @@ export class Cluster implements ClusterModel, ClusterState {
}
@action
async activate(force = false ) {
async activate(force = false) {
if (this.activated && !force) {
return this.pushState();
}
@ -340,7 +341,7 @@ export class Cluster implements ClusterModel, ClusterState {
for (const w of warnings) {
if (w.involvedObject.kind === 'Pod') {
try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace);
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if (podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
@ -351,11 +352,10 @@ export class Cluster implements ClusterModel, ClusterState {
uniqEventSources.add(w.involvedObject.uid);
}
}
let nodeNotificationCount = 0;
const nodes = (await client.listNode()).body.items;
nodes.map(n => {
nodeNotificationCount = nodeNotificationCount + getNodeWarningConditions(n).length
});
const nodeNotificationCount = nodes
.map(getNodeWarningConditions)
.reduce((sum, conditions) => sum + conditions.length, 0);
return uniqEventSources.size + nodeNotificationCount;
} catch (error) {
logger.error("Failed to fetch event count: " + JSON.stringify(error))
@ -371,7 +371,8 @@ export class Cluster implements ClusterModel, ClusterState {
workspace: this.workspace,
preferences: this.preferences,
metadata: this.metadata,
ownerRef: this.ownerRef
ownerRef: this.ownerRef,
accessibleNamespaces: this.accessibleNamespaces,
};
return toJS(model, {
recurseEverything: true
@ -426,6 +427,10 @@ export class Cluster implements ClusterModel, ClusterState {
}
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces
}
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
try {
const namespaceList = await api.listNamespace()
@ -442,7 +447,7 @@ export class Cluster implements ClusterModel, ClusterState {
} catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
if (ctx.namespace) return [ctx.namespace]
return []
return [];
}
}

View File

@ -21,6 +21,8 @@ import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { appEventBus } from "../common/event-bus"
import { extensionLoader } from "../extensions/extension-loader";
import { extensionManager } from "../extensions/extension-manager";
import { extensionsStore } from "../extensions/extensions-store";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@ -52,6 +54,7 @@ app.on("ready", async () => {
userStore.load(),
clusterStore.load(),
workspaceStore.load(),
extensionsStore.load(),
]);
// find free port
@ -76,7 +79,7 @@ app.on("ready", async () => {
}
LensExtensionsApi.windowManager = windowManager = new WindowManager(proxyPort);
extensionLoader.init(); // call after windowManager to see splash earlier
extensionLoader.init(await extensionManager.load()); // call after windowManager to see splash earlier
setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" })

View File

@ -1,7 +1,7 @@
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron"
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"
import { autorun } from "mobx";
import { WindowManager } from "./window-manager";
import { appName, isMac, isWindows } from "../common/vars";
import { appName, isMac, isWindows, isTestEnv } from "../common/vars";
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
@ -193,6 +193,12 @@ export function buildMenu(windowManager: WindowManager) {
navigate(whatsNewURL())
},
},
{
label: "Documentation",
click: async () => {
shell.openExternal('https://docs.k8slens.dev/');
},
},
...ignoreOnMac([
{
label: "About Lens",
@ -229,4 +235,38 @@ export function buildMenu(windowManager: WindowManager) {
const menu = Menu.buildFromTemplate(Object.values(appMenu));
Menu.setApplicationMenu(menu);
if (isTestEnv) {
// this is a workaround for the test environment (spectron) not being able to directly access
// the application menus (https://github.com/electron-userland/spectron/issues/21)
ipcMain.on('test-menu-item-click', (event: IpcMainEvent, ...names: string[]) => {
let menu: Menu = Menu.getApplicationMenu()
const parentLabels: string[] = [];
let menuItem: MenuItem
for (const name of names) {
parentLabels.push(name);
menuItem = menu?.items?.find(item => item.label === name);
if (!menuItem) {
break;
}
menu = menuItem.submenu;
}
const menuPath: string = parentLabels.join(" -> ")
if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
return;
}
const { enabled, visible, click } = menuItem;
if (enabled === false || visible === false || typeof click !== 'function') {
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
return;
}
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} click!`);
menuItem.click();
});
}
}

View File

@ -4,7 +4,7 @@ import http from "http"
import path from "path"
import { readFile } from "fs-extra"
import { Cluster } from "./cluster"
import { apiPrefix, appName, publicPath } from "../common/vars";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
export interface RouterRequestOpts {
@ -94,23 +94,35 @@ export class Router {
return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
}
async handleStaticFile(filePath: string, res: http.ServerResponse) {
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage) {
const asset = path.join(__static, filePath);
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
const toWebpackDevServer = filename.includes(appName) || filename.includes('hot-update') || req.url.includes('sockjs-node');
if (isDevelopment && toWebpackDevServer) {
const redirectLocation = `http://localhost:${webpackDevServerPort}` + req.url;
res.statusCode = 307;
res.setHeader('Location', redirectLocation);
res.end();
return;
}
const data = await readFile(asset);
res.setHeader("Content-Type", this.getMimeType(asset));
res.write(data)
res.end()
res.write(data);
res.end();
} catch (err) {
this.handleStaticFile(`${publicPath}/${appName}.html`, res);
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req);
}
}
protected addRoutes() {
// Static assets
this.router.add({ method: 'get', path: '/{path*}' }, ({ params, response }: LensApiRequest) => {
this.handleStaticFile(params.path, response);
});
this.router.add(
{ method: 'get', path: '/{path*}' },
({ params, response, path, raw: { req }}: LensApiRequest) => {
this.handleStaticFile(params.path, response, req);
});
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))

View File

@ -1,22 +1,13 @@
import type { KubeObjectStore } from "../kube-object.store";
import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuProps } from "../components/kube-object";
import type React from "react";
import { observable } from "mobx";
import { action, observable } from "mobx";
import { autobind } from "../utils";
import { KubeApi } from "./kube-api";
export interface ApiComponents {
List?: React.ComponentType<KubeObjectListLayoutProps>;
Menu?: React.ComponentType<KubeObjectMenuProps>;
Details?: React.ComponentType<KubeObjectDetailsProps>;
}
@autobind()
export class ApiManager {
private apis = observable.map<string, KubeApi>();
private stores = observable.map<KubeApi, KubeObjectStore>();
private views = observable.map<KubeApi, ApiComponents>();
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
if (typeof pathOrCallback === "string") {
@ -46,8 +37,11 @@ export class ApiManager {
}
}
registerStore(api: KubeApi, store: KubeObjectStore) {
this.stores.set(api, store);
@action
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
apis.forEach(api => {
this.stores.set(api, store);
})
}
getStore(api: string | KubeApi): KubeObjectStore {

View File

@ -5,6 +5,7 @@ export * from "./cluster.api"
export * from "./cluster-role.api"
export * from "./cluster-role-binding.api"
export * from "./configmap.api"
export * from "./crd.api"
export * from "./cron-job.api"
export * from "./daemon-set.api"
export * from "./deployment.api"

View File

@ -9,7 +9,6 @@ import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { apiKubePrefix, isDevelopment } from "../../common/vars";
import * as URL from "url"
export interface IKubeApiOptions<T extends KubeObject> {
apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods"

View File

@ -4,6 +4,8 @@ import React from "react";
import * as Mobx from "mobx"
import * as MobxReact from "mobx-react"
import * as LensExtensions from "../extensions/extension-api"
import { App } from "./components/app";
import { LensApp } from "./lens-app";
import { render, unmountComponentAtNode } from "react-dom";
import { isMac } from "../common/vars";
import { userStore } from "../common/user-store";
@ -11,8 +13,7 @@ import { workspaceStore } from "../common/workspace-store";
import { clusterStore } from "../common/cluster-store";
import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store";
import { App } from "./components/app";
import { LensApp } from "./lens-app";
import { extensionsStore } from "../extensions/extensions-store";
type AppComponent = React.ComponentType & {
init?(): Promise<void>;
@ -34,6 +35,7 @@ export async function bootstrap(App: AppComponent) {
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
extensionsStore.load(),
i18nStore.init(),
themeStore.init(),
]);

View File

@ -11,7 +11,7 @@ import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { SearchInput } from "../input";
import { SearchInputUrl } from "../input";
enum sortBy {
name = "name",
@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
(items: HelmChart[]) => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
<SearchInputUrl placeholder={_i18n._(t`Search Helm Charts`)} />
)}
renderTableHeader={[
{ className: "icon" },

View File

@ -0,0 +1,38 @@
import React from "react";
import { observer } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title";
import { EditableList } from "../../editable-list";
import { observable } from "mobx";
import { _i18n } from "../../../i18n";
import { Trans } from "@lingui/macro";
interface Props {
cluster: Cluster;
}
@observer
export class ClusterAccessibleNamespaces extends React.Component<Props> {
@observable namespaces = new Set(this.props.cluster.accessibleNamespaces);
render() {
return (
<>
<SubTitle title="Accessible Namespaces" />
<p><Trans>This setting is useful for manually specifying which namespaces you have access to. This is useful when you don't have permissions to list namespaces.</Trans></p>
<EditableList
placeholder={_i18n._("Add new namespace...")}
add={(newNamespace) => {
this.namespaces.add(newNamespace);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
items={Array.from(this.namespaces)}
remove={({ oldItem: oldNamesapce }) => {
this.namespaces.delete(oldNamesapce);
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
}}
/>
</>
);
}
}

View File

@ -6,6 +6,7 @@ import { ClusterIconSetting } from "./components/cluster-icon-setting";
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
interface Props {
cluster: Cluster;
@ -21,6 +22,7 @@ export class General extends React.Component<Props> {
<ClusterProxySetting cluster={this.props.cluster} />
<ClusterPrometheusSetting cluster={this.props.cluster} />
<ClusterHomeDirSetting cluster={this.props.cluster} />
<ClusterAccessibleNamespaces cluster={this.props.cluster} />
</div>;
}
}

View File

@ -107,4 +107,4 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
}
export const clusterStore = new ClusterStore();
apiManager.registerStore(clusterApi, clusterStore);
apiManager.registerStore(clusterStore);

View File

@ -9,4 +9,4 @@ export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> {
}
export const hpaStore = new HPAStore();
apiManager.registerStore(hpaApi, hpaStore);
apiManager.registerStore(hpaStore);

View File

@ -9,4 +9,4 @@ export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
}
export const configMapsStore = new ConfigMapsStore();
apiManager.registerStore(configMapApi, configMapsStore);
apiManager.registerStore(configMapsStore);

View File

@ -9,4 +9,4 @@ export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudg
}
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
apiManager.registerStore(pdbApi, podDisruptionBudgetsStore);
apiManager.registerStore(podDisruptionBudgetsStore);

View File

@ -9,4 +9,4 @@ export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
}
export const resourceQuotaStore = new ResourceQuotasStore();
apiManager.registerStore(resourceQuotaApi, resourceQuotaStore);
apiManager.registerStore(resourceQuotaStore);

View File

@ -9,4 +9,4 @@ export class SecretsStore extends KubeObjectStore<Secret> {
}
export const secretsStore = new SecretsStore();
apiManager.registerStore(secretsApi, secretsStore);
apiManager.registerStore(secretsStore);

View File

@ -10,7 +10,7 @@ import { KubeObject } from "../../api/kube-object";
import { ICRDRouteParams } from "./crd.route";
import { autorun, computed } from "mobx";
import { crdStore } from "./crd.store";
import { SortingCallback } from "../table";
import { TableSortCallback } from "../table";
import { apiManager } from "../../api/api-manager";
interface Props extends RouteComponentProps<ICRDRouteParams> {
@ -50,7 +50,7 @@ export class CrdResources extends React.Component<Props> {
if (!crd) return null;
const isNamespaced = crd.isNamespaced();
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
const sortingCallbacks: { [sortBy: string]: SortingCallback } = {
const sortingCallbacks: { [sortBy: string]: TableSortCallback } = {
[sortBy.name]: (item: KubeObject) => item.getName(),
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,

View File

@ -14,7 +14,7 @@ function initStore(crd: CustomResourceDefinition) {
const api = apiManager.getApi(apiBase) || new KubeApi({ apiBase, kind, isNamespaced });
if (!apiManager.getStore(api)) {
apiManager.registerStore(api, new CRDResourceStore(api));
apiManager.registerStore(new CRDResourceStore(api));
}
}
@ -64,4 +64,4 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
export const crdStore = new CRDStore();
apiManager.registerStore(crdApi, crdStore);
apiManager.registerStore(crdStore);

View File

@ -49,4 +49,4 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
}
export const eventStore = new EventStore();
apiManager.registerStore(eventApi, eventStore);
apiManager.registerStore(eventStore);

View File

@ -6,15 +6,14 @@ import { Trans } from "@lingui/macro";
import { KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
import { eventStore } from "./event.store";
interface Props {
export interface KubeEventDetailsProps {
object: KubeObject;
}
@observer
export class KubeEventDetails extends React.Component<Props> {
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
async componentDidMount() {
eventStore.loadAll();
}

View File

@ -2,11 +2,17 @@
--width: 100%;
--max-width: auto;
.extension {
--flex-gap: $padding / 3;
padding: $padding $padding * 2;
background: $colorVague;
border-radius: $radius;
.extension-list {
.extension {
--flex-gap: $padding / 3;
padding: $padding $padding * 2;
background: $colorVague;
border-radius: $radius;
&:not(:first-of-type) {
margin-top: $padding * 2;
}
}
}
.extensions-path {

View File

@ -19,7 +19,8 @@ export class Extensions extends React.Component {
@computed get extensions() {
const searchText = this.search.toLowerCase();
return extensionLoader.userExtensions.filter(({ name, description }) => {
return Array.from(extensionLoader.userExtensions.values()).filter(ext => {
const { name, description } = ext.manifest;
return [
name.toLowerCase().includes(searchText),
description.toLowerCase().includes(searchText),
@ -68,9 +69,10 @@ export class Extensions extends React.Component {
)
}
return extensions.map(ext => {
const { id, name, description, isEnabled } = ext;
const { manifestPath: extId, isEnabled, manifest } = ext;
const { name, description } = manifest;
return (
<div key={id} className="extension flex gaps align-center">
<div key={extId} className="extension flex gaps align-center">
<div className="box grow flex column gaps">
<div className="package">
Name: <code className="name">{name}</code>
@ -80,10 +82,10 @@ export class Extensions extends React.Component {
</div>
</div>
{!isEnabled && (
<Button plain active onClick={() => ext.enable()}>Enable</Button>
<Button plain active onClick={() => ext.isEnabled = true}>Enable</Button>
)}
{isEnabled && (
<Button accent onClick={() => ext.disable()}>Disable</Button>
<Button accent onClick={() => ext.isEnabled = false}>Disable</Button>
)}
</div>
)
@ -102,7 +104,7 @@ export class Extensions extends React.Component {
value={this.search}
onChange={(value) => this.search = value}
/>
<div className="extension-list flex column gaps">
<div className="extension-list">
{this.renderExtensions()}
</div>
</WizardLayout>

View File

@ -95,4 +95,4 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
export const namespaceStore = new NamespaceStore();
apiManager.registerStore(namespacesApi, namespaceStore);
apiManager.registerStore(namespaceStore);

Some files were not shown because too many files have changed in this diff Show More