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

Merge branch 'master' into hotbar-commands-and-navigate

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-04-21 06:49:40 +03:00
commit 6ec54b6485
66 changed files with 985 additions and 31576 deletions

View File

@ -35,6 +35,15 @@ jobs:
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages 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 - script: make node_modules
displayName: Install dependencies displayName: Install dependencies
- script: make build-npm - script: make build-npm
@ -51,7 +60,6 @@ jobs:
env: env:
WIN_CSC_LINK: $(WIN_CSC_LINK) WIN_CSC_LINK: $(WIN_CSC_LINK)
WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD) WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD)
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- job: macOS - job: macOS
@ -76,6 +84,15 @@ jobs:
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages 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 - script: make node_modules
displayName: Install dependencies displayName: Install dependencies
- script: make build-npm - script: make build-npm
@ -94,7 +111,6 @@ jobs:
APPLEIDPASS: $(APPLEIDPASS) APPLEIDPASS: $(APPLEIDPASS)
CSC_LINK: $(CSC_LINK) CSC_LINK: $(CSC_LINK)
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD) CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- job: Linux - job: Linux
@ -119,6 +135,15 @@ jobs:
yarn | "$(Agent.OS)" yarn | "$(Agent.OS)"
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages 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 - script: make node_modules
displayName: Install dependencies displayName: Install dependencies
- script: make build-npm - script: make build-npm
@ -143,7 +168,6 @@ jobs:
displayName: Build displayName: Build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env: env:
GH_TOKEN: $(GH_TOKEN)
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
- script: make publish-npm - 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 sudo chown -R $USER $HOME/.kube $HOME/.minikube
name: Install integration test dependencies name: Install integration test dependencies
if: runner.os == 'Linux' if: runner.os == 'Linux'
- run: | - run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
set -e
rm -rf extensions/telemetry
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
git checkout extensions/telemetry
name: Run Linux integration tests name: Run Linux integration tests
if: runner.os == 'Linux' if: runner.os == 'Linux'
- run: | - run: make integration-win
set -e
rm -rf extensions/telemetry
make integration-win
git checkout extensions/telemetry
name: Run Windows integration tests name: Run Windows integration tests
shell: bash shell: bash
if: runner.os == 'Windows' if: runner.os == 'Windows'
- run: | - run: make integration-mac
set -e
rm -rf extensions/telemetry
make integration-mac
git checkout extensions/telemetry
name: Run macOS integration tests name: Run macOS integration tests
if: runner.os == 'macOS' 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: Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository, if that * 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". 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 * Content outside of the above mentioned directories or restrictions above is
available under the "MIT" license as defined below. 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 .PHONY: build
build: node_modules binaries/client build-extensions build: node_modules binaries/client build-extensions
yarn run compile
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win ./node_modules/.bin/electron-builder --publish onTag --x64 --ia32
else else
yarn dist ./node_modules/.bin/electron-builder --publish onTag
endif endif
$(extension_node_modules): $(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) [![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) [![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) [![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 ## Installation
See [Getting Started](https://docs.k8slens.dev/latest/getting-started/) page. 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": { "ssri": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"figgy-pudding": "^3.5.1" "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": { "ssri": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"figgy-pudding": "^3.5.1" "figgy-pudding": "^3.5.1"

View File

@ -6337,9 +6337,9 @@
} }
}, },
"ssri": { "ssri": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"figgy-pudding": "^3.5.1" "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", () => { describe("preferences page", () => {
it('shows "preferences"', async () => { 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.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION"); 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"; import { exec } from "child_process";
const AppPaths: Partial<Record<NodeJS.Platform, string>> = { const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/Lens.exe", "win32": "./dist/win-unpacked/OpenLens.exe",
"linux": "./dist/linux-unpacked/kontena-lens", "linux": "./dist/linux-unpacked/open-lens",
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", "darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
}; };
interface DoneCallback { interface DoneCallback {

View File

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

View File

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

View File

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

View File

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

View File

@ -91,9 +91,9 @@ export class HelmReleaseManager {
return stdout; 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 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; return stdout;
} }

View File

@ -66,12 +66,12 @@ class HelmService {
return await releaseManager.getRelease(releaseName, namespace, cluster); 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(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release values"); 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) { public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {

View File

@ -101,10 +101,10 @@ class HelmApiRoute extends LensApi {
} }
public async getReleaseValues(request: LensApiRequest) { public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request; const { cluster, params, response, query } = request;
try { 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); this.respondText(response, result);
} catch (error) { } catch (error) {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog"; import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard"; 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 { releaseStore } from "./release.store";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
@ -39,7 +39,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
onOpen = async () => { onOpen = async () => {
this.isLoading = true; this.isLoading = true;
const currentRevision = this.release.getRevision(); 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 = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort releases = orderBy(releases, "revision", "desc"); // sort

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import "./preferences.scss"; import "./preferences.scss";
import React from "react"; import React from "react";
import moment from "moment-timezone";
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; 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() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => navigation.location.hash, hash => { reaction(() => navigation.location.hash, hash => {
@ -95,7 +101,7 @@ export class Preferences extends React.Component {
const { preferences } = userStore; const { preferences } = userStore;
const extensions = appPreferenceRegistry.getItems(); const extensions = appPreferenceRegistry.getItems();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry); 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 (!defaultShell) {
if (isWindows) { if (isWindows) {
@ -153,6 +159,18 @@ export class Preferences extends React.Component {
label="Automatically start Lens on login" label="Automatically start Lens on login"
/> />
</section> </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> </section>
)} )}
{this.activeTab == Pages.Proxy && ( {this.activeTab == Pages.Proxy && (

View File

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

View File

@ -6,9 +6,11 @@ import DOMPurify from "dompurify";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { action, computed, observable } from "mobx"; import { action, computed, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import moment from "moment-timezone";
import { Align, ListOnScrollProps } from "react-window"; import { Align, ListOnScrollProps } from "react-window";
import { SearchStore, searchStore } from "../../../common/search-store"; import { SearchStore, searchStore } from "../../../common/search-store";
import { userStore } from "../../../common/user-store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -79,12 +81,15 @@ export class LogList extends React.Component<Props> {
@computed @computed
get logs() { get logs() {
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps; const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
const { preferences } = userStore;
if (!showTimestamps) { if (!showTimestamps) {
return logStore.logsWithoutTimestamps; 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(); 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) { getTimestamps(logs: string) {
return logs.match(/^\d+\S+/gm); return logs.match(/^\d+\S+/gm);
} }

View File

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

View File

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

2050
yarn.lock

File diff suppressed because it is too large Load Diff