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

Merge branch 'master' into release/v5.0.0-alpha.2

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-04-20 22:09:44 +03:00
commit fa557ef697
69 changed files with 1022 additions and 31585 deletions

View File

@ -35,6 +35,15 @@ jobs:
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
cp -r .lens-ide-overlay/* ./
cp build/package.json.patch . && patch package.json package.json.patch
displayName: Customize config
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
@ -51,7 +60,6 @@ jobs:
env:
WIN_CSC_LINK: $(WIN_CSC_LINK)
WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD)
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- job: macOS
@ -76,6 +84,15 @@ jobs:
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
cp -r .lens-ide-overlay/* ./
cp build/package.json.patch . && patch package.json package.json.patch
displayName: Customize config
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
@ -94,7 +111,6 @@ jobs:
APPLEIDPASS: $(APPLEIDPASS)
CSC_LINK: $(CSC_LINK)
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- job: Linux
@ -119,6 +135,15 @@ jobs:
yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages
- bash: |
set -e
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
rm -rf .lens-ide-overlay/.git
cp -r .lens-ide-overlay/* ./
cp build/package.json.patch . && patch package.json package.json.patch
displayName: Customize config
env:
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
- script: make node_modules
displayName: Install dependencies
- script: make build-npm
@ -143,7 +168,6 @@ jobs:
displayName: Build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- script: make publish-npm

8
.bundled-extensions.json Normal file
View File

@ -0,0 +1,8 @@
{
"extensions": [
"pod-menu",
"node-menu",
"metrics-cluster-feature",
"kube-object-event-status"
]
}

View File

@ -77,27 +77,15 @@ jobs:
sudo chown -R $USER $HOME/.kube $HOME/.minikube
name: Install integration test dependencies
if: runner.os == 'Linux'
- run: |
set -e
rm -rf extensions/telemetry
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
git checkout extensions/telemetry
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
name: Run Linux integration tests
if: runner.os == 'Linux'
- run: |
set -e
rm -rf extensions/telemetry
make integration-win
git checkout extensions/telemetry
- run: make integration-win
name: Run Windows integration tests
shell: bash
if: runner.os == 'Windows'
- run: |
set -e
rm -rf extensions/telemetry
make integration-mac
git checkout extensions/telemetry
- run: make integration-mac
name: Run macOS integration tests
if: runner.os == 'macOS'

View File

@ -1,11 +1,9 @@
Copyright (c) 2021 Mirantis, Inc.
Copyright (c) 2021 OpenLens Authors.
Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository, if that
directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All third party components incorporated into the Lens Software are licensed
under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is
available under the "MIT" license as defined below.

View File

@ -65,10 +65,11 @@ integration-win: binaries/client build-extension-types build-extensions
.PHONY: build
build: node_modules binaries/client build-extensions
yarn run compile
ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win
./node_modules/.bin/electron-builder --publish onTag --x64 --ia32
else
yarn dist
./node_modules/.bin/electron-builder --publish onTag
endif
$(extension_node_modules):

View File

@ -1,32 +1,22 @@
# Lens | The Kubernetes IDE
# Lens Open Source Project (OpenLens)
[![Build Status](https://github.com/lensapp/lens/actions/workflows/test.yml/badge.svg)](https://github.com/lensapp/lens/actions/workflows/test.yml)
[![Releases](https://img.shields.io/github/downloads/lensapp/lens/total.svg)](https://github.com/lensapp/lens/releases?label=Downloads)
[![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack&longCache=true&style=flat)](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
Lens provides the full situational awareness for everything that runs in Kubernetes. It's lowering the barrier of entry for people just getting started and radically improving productivity for people with more experience.
## The Repository
The Lens open source project is backed by a number of Kubernetes and cloud native ecosystem pioneers. It's a standalone application for MacOS, Windows and Linux operating systems. Lens is 100% open source and free of charge for any purpose.
This repository ("OpenLens") is where Team Lens develops the [Lens IDE](https://k8slens.dev) product together with the community. It is backed by a number of Kubernetes and cloud native ecosystem pioneers. This source code is available to everyone under the [MIT license](./LICENSE).
## Lens - The Kubernetes IDE
Lens - The Kubernetes IDE ("Lens IDE") is a distribution of the OpenLens repository with Team Lens specific customizations released under a traditional [EULA](https://k8slens.dev/licenses/eula).
Lens IDE provides the full situational awareness for everything that runs in Kubernetes. It's lowering the barrier of entry for people just getting started and radically improving productivity for people with more experience.
Lens IDE a standalone application for MacOS, Windows and Linux operating systems. You can download it free of charge for Windows, MacOS, and Linux from [Lens IDE website](https://k8slens.dev).
[![Screenshot](.github/screenshot.png)](https://www.youtube.com/watch?v=eeDwdVXattc)
## What makes Lens special?
* Amazing usability and end-user experience
* Unified, secure, multi-cluster management on any platform: support for hundreds of clusters
* Standalone application: no need to install anything in-cluster
* Lens installs anywhere, elimanting the need to wrangle credentials
* Real-time cluster state visualization
* Resource utilization charts and trends with history powered by built-in Prometheus
* Smart terminal access to nodes and containers
* Clusters can be local (e.g. minikube) or external (e.g. EKS, GKE, AKS)
* Performance optimized to handle massive clusters (tested with a cluster running 25k pods)
* RBAC security is preserved, as Lens uses the standard Kubernetes API
* Lens Extensions are used to add custom visualizations and functionality to accelerate development workflows for all the technologies and services that integrate with Kubernetes
* Port forwarding
* Helm package deployment: Browse and deploy Helm charts with one click-Install
* Extensions via Lens Extensions API
## Installation
See [Getting Started](https://docs.k8slens.dev/latest/getting-started/) page.

View File

@ -1,2 +0,0 @@
node_modules/
dist/

View File

@ -1,11 +0,0 @@
# Lens Example Extension
*TODO*: add more info
## Build
`npm run build`
## Dev
`npm run dev`

View File

@ -1,11 +0,0 @@
import { LensMainExtension } from "@k8slens/extensions";
export default class ExampleExtensionMain extends LensMainExtension {
onActivate() {
console.log("EXAMPLE EXTENSION MAIN: ACTIVATED", this.name, this.id);
}
onDeactivate() {
console.log("EXAMPLE EXTENSION MAIN: DEACTIVATED", this.name, this.id);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
{
"name": "example-extension",
"version": "1.0.0",
"description": "Example extension",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"dependencies": {
"react-open-doodles": "^1.0.5"
},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -1,65 +0,0 @@
import React from "react";
import { observer } from "mobx-react";
import { CoffeeDoodle } from "react-open-doodles";
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
extension: LensRendererExtension; // provided in "./renderer.tsx"
}
export interface ExamplePageParams {
exampleId: string;
selectedNamespaces: K8sApi.Namespace[];
}
export const namespaceStore = K8sApi.apiManager.getStore<K8sApi.NamespaceStore>(K8sApi.namespacesApi);
@observer
export class ExamplePage extends React.Component<ExamplePageProps> {
async componentDidMount() {
await namespaceStore.loadAll();
}
deactivate = () => {
const { extension } = this.props;
extension.disable();
};
renderSelectedNamespaces() {
const { selectedNamespaces } = this.props.params;
return (
<div className="flex gaps inline">
{selectedNamespaces.get().map(ns => {
const name = ns.getName();
return <Component.Badge key={name} label={name} tooltip={`Created: ${ns.getAge()}`}/>;
})}
</div>
);
}
render() {
const { exampleId } = this.props.params;
return (
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
<div style={{ width: 200 }}>
<CoffeeDoodle accent="#3d90ce"/>
</div>
<div>Hello from Example extension!</div>
<div>Location: <i>{location.href}</i></div>
<div>Namespaces: {this.renderSelectedNamespaces()}</div>
<p className="url-params-demo flex column gaps">
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
{exampleId.get() === "secret" && (
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
)}
</p>
</div>
);
}
}

View File

@ -1,45 +0,0 @@
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
import { ExamplePage, ExamplePageParams, namespaceStore } from "./page";
import React from "react";
import path from "path";
export default class ExampleExtension extends LensRendererExtension {
clusterPages: Interface.PageRegistration[] = [
{
components: {
Page: (props: Interface.PageComponentProps<ExamplePageParams>) => {
return <ExamplePage {...props} extension={this}/>;
},
},
params: {
// setup basic param "exampleId" with default value "demo"
exampleId: "demo",
// setup advanced multi-values param "selectedNamespaces" with custom parsing/stringification
selectedNamespaces: {
defaultValueStringified: ["default", "kube-system"],
multiValues: true,
parse(values: string[]) { // from URL
return values.map(name => namespaceStore.getByName(name)).filter(Boolean);
},
stringify(values: K8sApi.Namespace[]) { // to URL
return values.map(namespace => namespace.getName());
},
}
}
}
];
clusterPageMenus: Interface.ClusterPageMenuRegistration[] = [
{
title: "Example extension",
components: {
Icon: ExampleIcon,
},
},
];
}
export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
}

View File

@ -1,26 +0,0 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -1,65 +0,0 @@
const path = require("path");
module.exports = [
{
entry: "./main.ts",
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"mobx": "var global.Mobx",
"react": "var global.React"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
},
{
entry: "./renderer.tsx",
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "renderer.js",
path: path.resolve(__dirname, "dist"),
},
},
];

View File

@ -2685,9 +2685,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"

View File

@ -1,13 +0,0 @@
import { LensMainExtension, Util } from "@k8slens/extensions";
export default class LicenseLensMainExtension extends LensMainExtension {
appMenus = [
{
parentId: "help",
label: "License",
async click() {
Util.openExternal("https://k8slens.dev/licenses/eula");
}
}
];
}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
{
"name": "lens-license",
"version": "0.1.0",
"description": "License menu item",
"main": "dist/main.js",
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/webpack": "^4.41.17",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
}
}

View File

@ -1,34 +0,0 @@
import path from "path";
const outputPath = path.resolve(__dirname, "dist");
export default [
{
entry: "./main.ts",
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: {
"@k8slens/extensions": "var global.LensExtensions",
"mobx": "var global.Mobx",
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "main.js",
path: outputPath,
},
},
];

View File

@ -5693,9 +5693,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"

View File

@ -6337,9 +6337,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"

View File

@ -1,9 +0,0 @@
import { LensMainExtension } from "@k8slens/extensions";
import { surveyPreferencesStore } from "./src/survey-preferences-store";
export default class SurveyMainExtension extends LensMainExtension {
async onActivate() {
await surveyPreferencesStore.loadExtension(this);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
{
"name": "lens-survey",
"version": "0.1.0",
"description": "Lens survey",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"got": "^11.8.1",
"jest": "^26.6.3",
"node-machine-id": "^1.1.12",
"react": "^16.13.1",
"refiner-js": "^1.0.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -1,25 +0,0 @@
import { LensRendererExtension } from "@k8slens/extensions";
import { survey } from "./src/survey";
import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference";
import { surveyPreferencesStore } from "./src/survey-preferences-store";
import React from "react";
export default class SurveyRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "In-App Surveys",
showInPreferencesTab: "telemetry",
components: {
Hint: () => <SurveyPreferenceHint/>,
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>
}
}
];
async onActivate() {
// Activate extension only on main renderer
if (window.location.hostname === "localhost") {
await surveyPreferencesStore.loadExtension(this);
survey.start();
}
}
}

View File

@ -1,3 +0,0 @@
declare module "refiner-js" {
export default function Refiner(key: string, value: string|object|number|Boolean|Array): void;
}

View File

@ -1,27 +0,0 @@
import { Component } from "@k8slens/extensions";
import React from "react";
import { observer } from "mobx-react";
import { SurveyPreferencesStore } from "./survey-preferences-store";
@observer
export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> {
render() {
const { survey } = this.props;
return (
<Component.Checkbox
label="Allow in-app surveys"
value={survey.enabled}
onChange={v => survey.enabled = v }
/>
);
}
}
export class SurveyPreferenceHint extends React.Component {
render() {
return (
<span>This will allow you to participate in surveys to improve the Lens experience.</span>
);
}
}

View File

@ -1,36 +0,0 @@
import { Store } from "@k8slens/extensions";
import { observable, toJS, when } from "mobx";
export type SurveyPreferencesModel = {
enabled: boolean;
};
export class SurveyPreferencesStore extends Store.ExtensionStore<SurveyPreferencesModel> {
@observable enabled = true;
whenEnabled = when(() => this.enabled);
private constructor() {
super({
configName: "preferences-store",
defaults: {
enabled: true
}
});
}
protected fromStore({ enabled }: SurveyPreferencesModel): void {
this.enabled = enabled;
}
toJSON(): SurveyPreferencesModel {
return toJS({
enabled: this.enabled
}, {
recurseEverything: true
});
}
}
export const surveyPreferencesStore = SurveyPreferencesStore.getInstance<SurveyPreferencesStore>();

View File

@ -1,46 +0,0 @@
import { Util } from "@k8slens/extensions";
import { machineId } from "node-machine-id";
import Refiner from "refiner-js";
import got from "got";
import { surveyPreferencesStore } from "./survey-preferences-store";
type SurveyIdResponse = {
surveyId: string;
};
export class Survey extends Util.Singleton {
static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43";
protected anonymousId: string;
private constructor() {
super();
}
async start() {
await surveyPreferencesStore.whenEnabled;
const surveyId = await this.fetchSurveyId();
if (surveyId) {
Refiner("setProject", Survey.PROJECT_ID);
Refiner("identifyUser", {
id: surveyId,
});
}
}
async fetchSurveyId() {
try {
const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev";
const anonymousId = await machineId();
const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"});
return (body as SurveyIdResponse).surveyId;
} catch(error) {
return null;
}
}
}
export const survey = Survey.getInstance<Survey>();

View File

@ -1,29 +0,0 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"../../types/*"
]
}
},
"include": [
"renderer.ts",
"src/**/*"
]
}

View File

@ -1,67 +0,0 @@
const path = require("path");
module.exports = [
{
entry: "./main.ts",
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
},
{
entry: "./renderer.tsx",
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "renderer.js",
path: path.resolve(__dirname, "dist"),
},
},
];

View File

@ -1,19 +0,0 @@
import { LensMainExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { tracker } from "./src/tracker";
export default class TelemetryMainExtension extends LensMainExtension {
async onActivate() {
console.log("telemetry main extension activated");
tracker.start();
tracker.reportPeriodically();
tracker.watchExtensions();
await telemetryPreferencesStore.loadExtension(this);
}
onDeactivate() {
tracker.stop();
console.log("telemetry main extension deactivated");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
{
"name": "lens-telemetry",
"version": "0.1.0",
"description": "Lens telemetry",
"main": "dist/main.js",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack -p",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"@types/analytics-node": "^3.1.3",
"analytics-node": "^3.4.0-beta.3",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"node-machine-id": "^1.1.12",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"universal-analytics": "^0.4.23",
"webpack": "^4.44.2"
}
}

View File

@ -1,25 +0,0 @@
import { LensRendererExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference";
import { tracker } from "./src/tracker";
import React from "react";
export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "Telemetry & Usage Tracking",
showInPreferencesTab: "telemetry",
id: "telemetry-tracking",
components: {
Hint: () => <TelemetryPreferenceHint/>,
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore}/>
}
}
];
async onActivate() {
console.log("telemetry extension activated");
tracker.start();
await telemetryPreferencesStore.loadExtension(this);
}
}

View File

@ -1,27 +0,0 @@
import { Component } from "@k8slens/extensions";
import React from "react";
import { observer } from "mobx-react";
import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
@observer
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
render() {
const { telemetry } = this.props;
return (
<Component.Checkbox
label="Allow telemetry & usage tracking"
value={telemetry.enabled}
onChange={v => { telemetry.enabled = v; }}
/>
);
}
}
export class TelemetryPreferenceHint extends React.Component {
render() {
return (
<span>Telemetry & usage data is collected to continuously improve the Lens experience.</span>
);
}
}

View File

@ -1,34 +0,0 @@
import { Store } from "@k8slens/extensions";
import { observable, toJS } from "mobx";
export type TelemetryPreferencesModel = {
enabled: boolean;
};
export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPreferencesModel> {
@observable enabled = true;
private constructor() {
super({
configName: "preferences-store",
defaults: {
enabled: true
}
});
}
protected fromStore({ enabled }: TelemetryPreferencesModel): void {
this.enabled = enabled;
}
toJSON(): TelemetryPreferencesModel {
return toJS({
enabled: this.enabled
}, {
recurseEverything: true
});
}
}
export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>();

View File

@ -1,185 +0,0 @@
import { EventBus, Util, Catalog, App } from "@k8slens/extensions";
import ua from "universal-analytics";
import Analytics from "analytics-node";
import { machineIdSync } from "node-machine-id";
import { telemetryPreferencesStore } from "./telemetry-preferences-store";
import { reaction, IReactionDisposer } from "mobx";
import { comparer } from "mobx";
export class Tracker extends Util.Singleton {
static readonly GA_ID = "UA-159377374-1";
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb";
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = [];
protected started = false;
protected visitor: ua.Visitor;
protected analytics: Analytics;
protected machineId: string = null;
protected ip: string = null;
protected appVersion: string;
protected locale: string;
protected userAgent: string;
protected anonymousId: string;
protected os: string;
protected disposers: IReactionDisposer[];
protected reportInterval: NodeJS.Timeout;
private constructor() {
super();
this.anonymousId = machineIdSync();
this.os = this.resolveOS();
this.userAgent = `Lens ${App.version} (${this.os})`;
try {
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
} catch (error) {
this.visitor = ua(Tracker.GA_ID);
}
this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 });
this.visitor.set("dl", "https://telemetry.k8slens.dev");
this.visitor.set("ua", this.userAgent);
this.disposers = [];
}
start() {
if (this.started === true) { return; }
this.started = true;
const handler = (ev: EventBus.AppEvent) => {
this.event(ev.name, ev.action, ev.params);
};
this.eventHandlers.push(handler);
EventBus.appEventBus.addListener(handler);
}
watchExtensions() {
let previousExtensions = App.getEnabledExtensions();
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
removedExtensions.forEach(ext => {
this.event("extension", "disable", { extension: ext });
});
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
newExtensions.forEach(ext => {
this.event("extension", "enable", { extension: ext });
});
previousExtensions = currentExtensions;
}, { equals: comparer.structural }));
}
reportPeriodically() {
this.reportData();
this.reportInterval = setInterval(() => {
this.reportData();
}, 60 * 60 * 1000); // report every 1h
}
stop() {
if (!this.started) { return; }
this.started = false;
for (const handler of this.eventHandlers) {
EventBus.appEventBus.removeListener(handler);
}
if (this.reportInterval) {
clearInterval(this.reportInterval);
}
this.disposers.forEach(disposer => {
disposer();
});
}
protected async isTelemetryAllowed(): Promise<boolean> {
return telemetryPreferencesStore.enabled;
}
protected reportData() {
const clustersList = Catalog.catalogEntities.getItemsForApiKind<Catalog.KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
this.event("generic-data", "report", {
appVersion: App.version,
os: this.os,
clustersCount: clustersList.length,
extensions: App.getEnabledExtensions()
});
clustersList.forEach((cluster) => {
if (!cluster?.metadata.lastSeen) { return; }
this.reportClusterData(cluster);
});
}
protected reportClusterData(cluster: Catalog.KubernetesCluster) {
this.event("cluster-data", "report", {
id: cluster.metadata.id,
managed: cluster.metadata.source !== "local",
kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes,
lastSeen: cluster.metadata.lastSeen,
prometheus: cluster.metadata.prometheus
});
}
protected resolveOS() {
let os = "";
if (App.isMac) {
os = "MacOS";
} else if(App.isWindows) {
os = "Windows";
} else if (App.isLinux) {
os = "Linux";
if (App.isSnap) {
os += "; Snap";
} else {
os += "; AppImage";
}
} else {
os = "Unknown";
}
return os;
}
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
try {
const allowed = await this.isTelemetryAllowed();
if (!allowed) {
return;
}
this.visitor.event({
ec: eventCategory,
ea: eventAction,
...otherParams,
}).send();
this.analytics.track({
anonymousId: this.anonymousId,
event: `${eventCategory} ${eventAction}`,
context: {
userAgent: this.userAgent,
},
properties: {
category: eventCategory,
...otherParams,
},
});
} catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err);
}
}
}
export const tracker = Tracker.getInstance<Tracker>();

View File

@ -1,29 +0,0 @@
{
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"../../types/*"
]
}
},
"include": [
"renderer.ts",
"src/**/*"
]
}

View File

@ -1,67 +0,0 @@
const path = require("path");
module.exports = [
{
entry: "./main.ts",
context: __dirname,
target: "electron-main",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
},
{
entry: "./renderer.tsx",
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "renderer.js",
path: path.resolve(__dirname, "dist"),
},
},
];

View File

@ -41,7 +41,7 @@ describe("Lens integration tests", () => {
describe("preferences page", () => {
it('shows "preferences"', async () => {
const appName: string = process.platform === "darwin" ? "Lens" : "File";
const appName: string = process.platform === "darwin" ? "OpenLens" : "File";
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION");

View File

@ -3,9 +3,9 @@ import * as util from "util";
import { exec } from "child_process";
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/Lens.exe",
"linux": "./dist/linux-unpacked/kontena-lens",
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
"win32": "./dist/win-unpacked/OpenLens.exe",
"linux": "./dist/linux-unpacked/open-lens",
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
};
interface DoneCallback {

View File

@ -1,14 +1,13 @@
{
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"name": "open-lens",
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"version": "5.0.0-alpha.2",
"main": "static/build/main.js",
"copyright": "© 2021, Mirantis, Inc.",
"copyright": "© 2021, OpenLens Authors",
"license": "MIT",
"author": {
"name": "Mirantis, Inc.",
"email": "info@k8slens.dev"
"name": "OpenLens Authors"
},
"scripts": {
"dev": "concurrently -k \"yarn run dev-run -C\" yarn:dev:*",
@ -23,9 +22,9 @@
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
"build:linux": "yarn run compile && electron-builder --linux --dir",
"build:mac": "yarn run compile && electron-builder --mac --dir",
"build:win": "yarn run compile && electron-builder --win --dir",
"integration": "jest --runInBand integration",
"dist": "yarn run compile && electron-builder --publish onTag",
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
@ -107,7 +106,6 @@
"target": [
"deb",
"rpm",
"snap",
"AppImage"
],
"extraResources": [
@ -161,16 +159,6 @@
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"snap": {
"confinement": "classic"
},
"publish": [
{
"provider": "s3",
"bucket": "lens-binaries",
"path": "/ide"
}
],
"protocols": {
"name": "Lens Protocol Handler",
"schemes": [
@ -179,17 +167,6 @@
"role": "Viewer"
}
},
"lens": {
"extensions": [
"telemetry",
"pod-menu",
"node-menu",
"metrics-cluster-feature",
"license-menu-item",
"kube-object-event-status",
"survey"
]
},
"dependencies": {
"@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3",
@ -224,8 +201,9 @@
"mobx-react": "^6.2.2",
"mock-fs": "^4.12.0",
"moment": "^2.26.0",
"moment-timezone": "^0.5.33",
"node-pty": "^0.9.0",
"npm": "^6.14.8",
"npm": "7.9",
"openid-client": "^3.15.2",
"p-limit": "^3.1.0",
"path-to-regexp": "^6.1.0",

View File

@ -0,0 +1,20 @@
import mockFs from "mock-fs";
import { HotbarStore, hotbarStore } from "../hotbar-store";
describe("HotbarStore", () => {
beforeEach(() => {
HotbarStore.resetInstance();
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
});
afterEach(() => {
mockFs.restore();
});
describe("load", () => {
it("loads one hotbar by default", () => {
hotbarStore.load();
expect(hotbarStore.hotbars.length).toEqual(1);
});
});
});

View File

@ -35,10 +35,14 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
this.hotbars = data.hotbars || [{
name: "default",
items: []
}];
if (data.hotbars?.length === 0) {
this.hotbars = [{
name: "default",
items: []
}];
} else {
this.hotbars = data.hotbars;
}
}
getByName(name: string) {

View File

@ -3,6 +3,7 @@ import { app, remote } from "electron";
import semver from "semver";
import { readFile } from "fs-extra";
import { action, computed, observable, reaction, toJS } from "mobx";
import moment from "moment-timezone";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version";
@ -23,6 +24,7 @@ export interface UserPreferences {
httpsProxy?: string;
shell?: string;
colorTheme?: string;
localeTimezone?: string;
allowUntrustedCAs?: boolean;
allowTelemetry?: boolean;
downloadMirror?: string | "default";
@ -54,6 +56,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
allowTelemetry: true,
allowUntrustedCAs: false,
colorTheme: UserStore.defaultTheme,
localeTimezone: moment.tz.guess(true) || "UTC",
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
openAtLogin: false,
@ -75,7 +78,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
// open at system start-up
reaction(() => this.preferences.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"]
@ -130,6 +133,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.lastSeenAppVersion = getAppVersion();
}
@action
setLocaleTimezone(tz: string) {
this.preferences.localeTimezone = tz;
}
protected refreshNewContexts = async () => {
try {
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");

View File

@ -9,10 +9,6 @@ export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
export function getBundledExtensions(): string[] {
return packageInfo.lens?.extensions || [];
}
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
const response = await requestPromise({
method: "GET",

View File

@ -6,7 +6,6 @@ import { observable, reaction, toJS, when } from "mobx";
import os from "os";
import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { getBundledExtensions } from "../common/utils/app-version";
import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer";
import { extensionsStore } from "./extensions-store";
@ -348,7 +347,7 @@ export class ExtensionDiscovery {
await this.installBundledPackages(this.packageJsonPath, bundledExtensions);
const userExtensions = await this.loadFromFolder(this.localFolderPath);
const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
for (const extension of userExtensions) {
if (await fs.pathExists(extension.manifestPath) === false) {
@ -382,14 +381,9 @@ export class ExtensionDiscovery {
async loadBundledExtensions() {
const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) {
continue;
}
const absPath = path.resolve(folderPath, fileName);
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
@ -402,8 +396,7 @@ export class ExtensionDiscovery {
return extensions;
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions();
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
const extensions: InstalledExtension[] = [];
const paths = await fs.readdir(folderPath);

View File

@ -91,9 +91,9 @@ export class HelmReleaseManager {
return stdout;
}
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
public async getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath();
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout;
}

View File

@ -66,12 +66,12 @@ class HelmService {
return await releaseManager.getRelease(releaseName, namespace, cluster);
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release values");
return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig);
return await releaseManager.getValues(releaseName, namespace, all, proxyKubeconfig);
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {

View File

@ -101,10 +101,10 @@ class HelmApiRoute extends LensApi {
}
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request;
const { cluster, params, response, query } = request;
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
this.respondText(response, result);
} catch (error) {

View File

@ -6,6 +6,7 @@ import { apiBase } from "../index";
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
import { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object";
import { JsonApiData } from "../json-api";
interface IReleasePayload {
name: string;
@ -68,73 +69,70 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
}
) => string;
export const helmReleasesApi = {
list(namespace?: string) {
return apiBase
.get<HelmRelease[]>(endpoint({ namespace }))
.then(releases => releases.map(HelmRelease.create));
},
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
get(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return releases.map(HelmRelease.create);
}
return apiBase.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
const path = endpoint({ name, namespace });
return {
...details,
resources
};
});
},
const details = await apiBase.get<IReleaseRawDetails>(path);
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return {
...details,
resources
};
}
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.post(endpoint(), { data });
},
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.post(endpoint(), { data });
}
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.put(endpoint({ name, namespace }), { data });
},
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiBase.put(endpoint({ name, namespace }), { data });
}
return apiBase.del(path);
},
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
const path = endpoint({ name, namespace });
getValues(name: string, namespace: string) {
const path = `${endpoint({ name, namespace })}/values`;
return apiBase.del(path);
}
return apiBase.get<string>(path);
},
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
const path = `${endpoint({ name, namespace })}/values${all? "?all": ""}`;
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = `${endpoint({ name, namespace })}/history`;
return apiBase.get<string>(path);
}
return apiBase.get(path);
},
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = `${endpoint({ name, namespace })}/history`;
rollback(name: string, namespace: string, revision: number) {
const path = `${endpoint({ name, namespace })}/rollback`;
return apiBase.get(path);
}
return apiBase.put(path, {
data: {
revision
}
});
}
};
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
const path = `${endpoint({ name, namespace })}/rollback`;
return apiBase.put(path, {
data: {
revision
}
});
}
@autobind()
export class HelmRelease implements ItemObject {

View File

@ -6,7 +6,7 @@ import isEqual from "lodash/isEqual";
import { observable, reaction } from "mobx";
import { Link } from "react-router-dom";
import kebabCase from "lodash/kebabCase";
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
import { HelmReleaseMenu } from "./release-menu";
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
@ -25,6 +25,7 @@ import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object";
import { Checkbox } from "../checkbox";
interface Props {
release: HelmRelease;
@ -35,6 +36,8 @@ interface Props {
export class ReleaseDetails extends Component<Props> {
@observable details: IReleaseDetails;
@observable values = "";
@observable valuesLoading = false;
@observable userSuppliedOnly = false;
@observable saving = false;
@observable releaseSecret: Secret;
@ -65,14 +68,16 @@ export class ReleaseDetails extends Component<Props> {
const { release } = this.props;
this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
this.details = await getRelease(release.getName(), release.getNs());
}
async loadValues() {
const { release } = this.props;
this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
this.valuesLoading = true;
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.userSuppliedOnly)) ?? "";
this.valuesLoading = false;
}
updateValues = async () => {
@ -107,21 +112,34 @@ export class ReleaseDetails extends Component<Props> {
};
renderValues() {
const { values, saving } = this;
const { values, valuesLoading, saving } = this;
return (
<div className="values">
<DrawerTitle title="Values"/>
<div className="flex column gaps">
<AceEditor
mode="yaml"
value={values}
onChange={values => this.values = values}
<Checkbox
label="User-supplied values only"
value={this.userSuppliedOnly}
onChange={values => {
this.userSuppliedOnly = values;
this.loadValues();
}}
disabled={valuesLoading}
/>
{valuesLoading
? <Spinner />
: <AceEditor
mode="yaml"
value={values}
onChange={values => this.values = values}
/>
}
<Button
primary
label="Save"
waiting={saving}
disabled={valuesLoading}
onClick={this.updateValues}
/>
</div>

View File

@ -5,7 +5,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store";
import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications";
@ -39,7 +39,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
onOpen = async () => {
this.isLoading = true;
const currentRevision = this.release.getRevision();
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort

View File

@ -1,7 +1,7 @@
import isEqual from "lodash/isEqual";
import { action, observable, reaction, when } from "mobx";
import { autobind } from "../../utils";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
@ -88,16 +88,16 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll) {
return helmReleasesApi.list();
} else {
return Promise // load resources per namespace
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat());
return listReleases();
}
return Promise // load resources per namespace
.all(namespaces.map(namespace => listReleases(namespace)))
.then(items => items.flat());
}
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
const response = await createRelease(payload);
if (this.isLoaded) this.loadFromContextNamespaces();
@ -105,7 +105,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
const response = await updateRelease(name, namespace, payload);
if (this.isLoaded) this.loadFromContextNamespaces();
@ -113,7 +113,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
const response = await rollbackRelease(name, namespace, revision);
if (this.isLoaded) this.loadFromContextNamespaces();
@ -121,7 +121,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async remove(release: HelmRelease) {
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs()));
}
async removeSelectedItems() {

View File

@ -11,6 +11,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { LocaleDate } from "../locale-date";
interface Props extends KubeObjectDetailsProps<KubeEvent> {
}
@ -38,10 +39,10 @@ export class EventDetails extends React.Component<Props> {
{event.getSource()}
</DrawerItem>
<DrawerItem name="First seen">
{event.getFirstSeenTime()} ago {event.firstTimestamp}
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />)
</DrawerItem>
<DrawerItem name="Last seen">
{event.getLastSeenTime()} ago {event.lastTimestamp}
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />)
</DrawerItem>
<DrawerItem name="Count">
{count}

View File

@ -1,6 +1,7 @@
import "./preferences.scss";
import React from "react";
import moment from "moment-timezone";
import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
@ -40,6 +41,11 @@ export class Preferences extends React.Component {
}));
}
timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
label: zone,
value: zone,
}));
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => navigation.location.hash, hash => {
@ -95,7 +101,7 @@ export class Preferences extends React.Component {
const { preferences } = userStore;
const extensions = appPreferenceRegistry.getItems();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry);
let defaultShell = process.env.SHELL || process.env.PTYSHELL;
let defaultShell = process.env.SHELL || process.env.PTYSHELL;
if (!defaultShell) {
if (isWindows) {
@ -153,6 +159,18 @@ export class Preferences extends React.Component {
label="Automatically start Lens on login"
/>
</section>
<hr />
<section id="locale">
<SubTitle title="Locale Timezone" />
<Select
options={this.timezoneOptions}
value={preferences.localeTimezone}
onChange={({ value }: SelectOption) => userStore.setLocaleTimezone(value)}
themeName="lens"
/>
</section>
</section>
)}
{this.activeTab == Pages.Proxy && (

View File

@ -13,6 +13,7 @@ import { IMetrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { clusterStore } from "../../../common/cluster-store";
import { LocaleDate } from "../locale-date";
interface Props {
pod: Pod;
@ -39,8 +40,8 @@ export class PodDetailsContainer extends React.Component<Props> {
<span>
{lastState}<br/>
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/>
Started at: {status.lastState.terminated.startedAt}<br/>
Finished at: {status.lastState.terminated.finishedAt}<br/>
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br/>
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br/>
</span>
);
}

View File

@ -6,9 +6,11 @@ import DOMPurify from "dompurify";
import debounce from "lodash/debounce";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import moment from "moment-timezone";
import { Align, ListOnScrollProps } from "react-window";
import { SearchStore, searchStore } from "../../../common/search-store";
import { userStore } from "../../../common/user-store";
import { cssNames } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
@ -79,12 +81,15 @@ export class LogList extends React.Component<Props> {
@computed
get logs() {
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
const { preferences } = userStore;
if (!showTimestamps) {
return logStore.logsWithoutTimestamps;
}
return this.props.logs;
return this.props.logs
.map(log => logStore.splitOutTimestamp(log))
.map(([logTimestamp, log]) => (`${moment.tz(logTimestamp, preferences.localeTimezone).format()}${log}`));
}
/**

View File

@ -146,6 +146,16 @@ export class LogStore {
return stamp.toISOString();
}
splitOutTimestamp(logs: string): [string, string] {
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
if (!extraction || extraction.length < 3) {
return ["", ""];
}
return [extraction[1], extraction[2]];
}
getTimestamps(logs: string) {
return logs.match(/^\d+\S+/gm);
}

View File

@ -1,7 +1,7 @@
import { action, autorun, IReactionDisposer, reaction } from "mobx";
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
import { DockTabStore } from "./dock-tab.store";
import { HelmRelease, helmReleasesApi } from "../../api/endpoints/helm-releases.api";
import { getReleaseValues, HelmRelease } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "../+apps-releases/release.store";
export interface IChartUpgradeData {
@ -89,7 +89,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
async loadValues(tabId: TabId) {
this.values.clearData(tabId); // reset
const { releaseName, releaseNamespace } = this.getData(tabId);
const values = await helmReleasesApi.getValues(releaseName, releaseNamespace);
const values = await getReleaseValues(releaseName, releaseNamespace, true);
this.values.setData(tabId, values);
}

View File

@ -14,20 +14,24 @@ interface Props {
@observer
export class HotbarMenu extends React.Component<Props> {
render() {
const { className } = this.props;
get hotbarItems() {
const hotbar = hotbarStore.getByName("default"); // FIXME
if (!hotbar) {
return null;
return [];
}
const items = hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean);
return hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean);
}
render() {
const { className } = this.props;
return (
<div className={cssNames("HotbarMenu flex column", className)}>
<div className="items flex column gaps">
{items.map((entity, index) => {
{this.hotbarItems.map((entity, index) => {
return (
<HotbarIcon
key={index}

View File

@ -4,6 +4,7 @@ import { DrawerItem, DrawerItemLabels } from "../drawer";
import { lookupApiLink } from "../../api/kube-api";
import { Link } from "react-router-dom";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { LocaleDate } from "../locale-date";
import { getDetailsUrl } from "./kube-object-details";
export interface KubeObjectMetaProps {
@ -35,7 +36,7 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
return (
<>
<DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}>
{getAge(true, false)} ago ({creationTimestamp})
{getAge(true, false)} ago ({<LocaleDate date={creationTimestamp} />})
</DrawerItem>
<DrawerItem name="Name" hidden={this.isHidden("name")}>
{getName()} <KubeObjectStatusIcon key="icon" object={object} />

View File

@ -0,0 +1 @@
export * from "./locale-date";

View File

@ -0,0 +1,18 @@
import React from "react";
import { observer } from "mobx-react";
import moment from "moment-timezone";
import { userStore } from "../../../common/user-store";
interface Props {
date: string
}
@observer
export class LocaleDate extends React.Component<Props> {
render() {
const { preferences } = userStore;
const { date } = this.props;
return <>{moment.tz(date, preferences.localeTimezone).format()}</>;
}
}

View File

@ -68,7 +68,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
extractComments: {
condition: "some",
banner: [
`Lens - The Kubernetes IDE. Copyright ${new Date().getFullYear()} by Mirantis, Inc. All rights reserved.`
`OpenLens - Open Source Kubernetes IDE. Copyright ${new Date().getFullYear()} OpenLens Authors`
].join("\n")
}
})

2050
yarn.lock

File diff suppressed because it is too large Load Diff