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

Merge branch 'in-app-survey' of github.com:lensapp/lens into in-app-survey

This commit is contained in:
Lauri Nevala 2021-02-04 12:40:24 +02:00
commit eb653539be
7 changed files with 118 additions and 76 deletions

View File

@ -12,6 +12,7 @@ categories:
- 'chore' - 'chore'
- 'area/ci' - 'area/ci'
- 'area/tests' - 'area/tests'
- 'dependencies'
template: | template: |
## Changes since $PREVIOUS_TAG ## Changes since $PREVIOUS_TAG
@ -20,8 +21,10 @@ template: |
### Download ### Download
- [Lens v$RESOLVED_VERSION - Linux](https://snapcraft.io/kontena-lens) - Lens v$RESOLVED_VERSION - Linux
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.AppImage) - [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
- [Snapcraft](https://snapcraft.io/kontena-lens) - [Snapcraft](https://snapcraft.io/kontena-lens)
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg) - [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe) - [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)

View File

@ -4,7 +4,7 @@ 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/Lens.exe",
"linux": "./dist/linux-unpacked/lens", "linux": "./dist/linux-unpacked/kontena-lens",
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
}; };

View File

@ -2,7 +2,7 @@
"name": "kontena-lens", "name": "kontena-lens",
"productName": "Lens", "productName": "Lens",
"description": "Lens - The Kubernetes IDE", "description": "Lens - The Kubernetes IDE",
"version": "4.1.0-alpha.1", "version": "4.1.0-alpha.2",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.", "copyright": "© 2020, Mirantis, Inc.",
"license": "MIT", "license": "MIT",
@ -103,7 +103,6 @@
], ],
"linux": { "linux": {
"category": "Network", "category": "Network",
"executableName": "lens",
"artifactName": "${productName}-${version}.${arch}.${ext}", "artifactName": "${productName}-${version}.${arch}.${ext}",
"target": [ "target": [
"deb", "deb",
@ -162,16 +161,16 @@
"oneClick": false, "oneClick": false,
"allowToChangeInstallationDirectory": true "allowToChangeInstallationDirectory": true
}, },
"snap": {
"confinement": "classic"
},
"publish": [ "publish": [
{ {
"provider": "github", "provider": "github",
"repo": "lens", "repo": "lens",
"owner": "lensapp" "owner": "lensapp"
} }
], ]
"snap": {
"confinement": "classic"
}
}, },
"lens": { "lens": {
"extensions": [ "extensions": [

View File

@ -13,17 +13,14 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
interface Props extends SelectProps { interface Props extends SelectProps {
showIcons?: boolean; showIcons?: boolean;
showClusterOption?: boolean; // show cluster option on the top (default: false) showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster") showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
customizeOptions?(nsOptions: SelectOption[]): SelectOption[]; customizeOptions?(options: SelectOption[]): SelectOption[];
} }
const defaultProps: Partial<Props> = { const defaultProps: Partial<Props> = {
showIcons: true, showIcons: true,
showClusterOption: false, showClusterOption: false,
get clusterOptionLabel() {
return `Cluster`;
},
}; };
@observer @observer
@ -39,13 +36,17 @@ export class NamespaceSelect extends React.Component<Props> {
} }
@computed get options(): SelectOption[] { @computed get options(): SelectOption[] {
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props; const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() })); let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
options = customizeOptions ? customizeOptions(options) : options; if (showAllNamespacesOption) {
options.unshift({ label: "All Namespaces", value: "" });
} else if (showClusterOption) {
options.unshift({ label: "Cluster", value: "" });
}
if (showClusterOption) { if (customizeOptions) {
options.unshift({ value: null, label: clusterOptionLabel }); options = customizeOptions(options);
} }
return options; return options;
@ -64,7 +65,7 @@ export class NamespaceSelect extends React.Component<Props> {
}; };
render() { render() {
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props; const { className, showIcons, customizeOptions, ...selectProps } = this.props;
return ( return (
<Select <Select
@ -80,32 +81,56 @@ export class NamespaceSelect extends React.Component<Props> {
@observer @observer
export class NamespaceSelectFilter extends React.Component { export class NamespaceSelectFilter extends React.Component {
@computed get placeholder(): React.ReactNode {
const namespaces = namespaceStore.getContextNamespaces();
switch (namespaces.length) {
case namespaceStore.allowedNamespaces.length:
return <>All namespaces</>;
case 0:
return <>Select a namespace</>;
case 1:
return <>Namespace: {namespaces[0]}</>;
default:
return <>Namespaces: {namespaces.join(", ")}</>;
}
}
formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
if (namespace) {
const isSelected = namespaceStore.hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}
return label;
};
onChange = ([{ value: namespace }]: SelectOption[]) => {
if (namespace) {
namespaceStore.toggleContext(namespace);
} else {
namespaceStore.toggleAll(); // "All namespaces" option clicked
}
};
render() { render() {
const { contextNs, hasContext, toggleContext } = namespaceStore;
let placeholder = <>All namespaces</>;
if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}</>;
if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}</>;
return ( return (
<NamespaceSelect <NamespaceSelect
placeholder={placeholder} isMulti={true}
showAllNamespacesOption={true}
closeMenuOnSelect={false} closeMenuOnSelect={false}
isOptionSelected={() => false} isOptionSelected={() => false}
controlShouldRenderValue={false} controlShouldRenderValue={false}
isMulti placeholder={this.placeholder}
onChange={([{ value }]: SelectOption[]) => toggleContext(value)} onChange={this.onChange}
formatOptionLabel={({ value: namespace }: SelectOption) => { formatOptionLabel={this.formatOptionLabel}
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}}
/> />
); );
} }

View File

@ -1,4 +1,4 @@
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
@ -6,7 +6,7 @@ import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces"); const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({ export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces", name: "namespaces",
@ -34,7 +34,7 @@ export function getDummyNamespace(name: string) {
export class NamespaceStore extends KubeObjectStore<Namespace> { export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi; api = namespacesApi;
@observable contextNs = observable.array<string>(); @observable private contextNs = observable.set<string>();
@observable isReady = false; @observable isReady = false;
whenReady = when(() => this.isReady); whenReady = when(() => this.isReady);
@ -57,7 +57,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
} }
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
return reaction(() => this.contextNs.toJS(), callback, { return reaction(() => Array.from(this.contextNs), callback, {
equals: comparer.shallow, equals: comparer.shallow,
...opts, ...opts,
}); });
@ -79,42 +79,32 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
get allowedNamespaces(): string[] { @computed get allowedNamespaces(): string[] {
return toJS(getHostedCluster().allowedNamespaces); return toJS(getHostedCluster().allowedNamespaces);
} }
@computed
private get initialNamespaces(): string[] { private get initialNamespaces(): string[] {
const allowed = new Set(this.allowedNamespaces); const namespaces = new Set(this.allowedNamespaces);
const prevSelected = storage.get(); const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
if (Array.isArray(prevSelected)) { // return previously saved namespaces from local-storage
return prevSelected.filter(namespace => allowed.has(namespace)); if (prevSelected.length > 0) {
return prevSelected;
} }
// otherwise select "default" or first allowed namespace // otherwise select "default" or first allowed namespace
if (allowed.has("default")) { if (namespaces.has("default")) {
return ["default"]; return ["default"];
} else if (allowed.size) { } else if (namespaces.size) {
return [Array.from(allowed)[0]]; return [Array.from(namespaces)[0]];
} }
return []; return [];
} }
getContextNamespaces(): string[] { public getContextNamespaces(): string[] {
const namespaces = this.contextNs.toJS(); return Array.from(this.contextNs);
// show all namespaces when nothing selected
if (!namespaces.length) {
if (this.isLoaded) {
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
return this.items.map(namespace => namespace.getName());
}
return this.allowedNamespaces;
}
return namespaces;
} }
getSubscribeApis() { getSubscribeApis() {
@ -143,26 +133,46 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
} }
@action @action
setContext(namespaces: string[]) { setContext(namespace: string | string[]) {
const namespaces = [namespace].flat();
this.contextNs.replace(namespaces); this.contextNs.replace(namespaces);
} }
hasContext(namespace: string | string[]) { hasContext(namespaces: string | string[]) {
const context = Array.isArray(namespace) ? namespace : [namespace]; return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
}
return context.every(namespace => this.contextNs.includes(namespace)); @computed get hasAllContexts(): boolean {
return this.contextNs.size === this.allowedNamespaces.length;
} }
@action @action
toggleContext(namespace: string) { toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace); if (this.hasContext(namespace)) {
else this.contextNs.push(namespace); this.contextNs.delete(namespace);
} else {
this.contextNs.add(namespace);
}
}
@action
toggleAll(showAll?: boolean) {
if (typeof showAll === "boolean") {
if (showAll) {
this.setContext(this.allowedNamespaces);
} else {
this.contextNs.clear();
}
} else {
this.toggleAll(!this.hasAllContexts);
}
} }
@action @action
async remove(item: Namespace) { async remove(item: Namespace) {
await super.remove(item); await super.remove(item);
this.contextNs.remove(item.getName()); this.contextNs.delete(item.getName());
} }
} }

View File

@ -30,7 +30,7 @@ export class PageFiltersStore {
protected syncWithContextNamespace() { protected syncWithContextNamespace() {
const disposers = [ const disposers = [
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => { reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
if (filteredNs.length !== namespaceStore.contextNs.length) { if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
namespaceStore.setContext(filteredNs); namespaceStore.setContext(filteredNs);
} }
}), }),

View File

@ -2,13 +2,16 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 4.1.0-alpha.1 (current version) ## 4.1.0-alpha.2 (current version)
- Change: list views default to a namespace (insted of listing resources from all namespaces) - Change: list views default to a namespace (insted of listing resources from all namespaces)
- Command palette
- Generic logs view with Pod selector - Generic logs view with Pod selector
- Possibility to add custom Helm repository through Lens - Possibility to add custom Helm repository through Lens
- Possibility to change visibility of Pod list columns - Possibility to change visibility of common resource list columns
- Suspend / resume buttons for CronJobs - Suspend / resume buttons for CronJobs
- Allow namespace to specified on role creation
- Allow for changing installation directory on Windows
- Dock tabs context menu - Dock tabs context menu
- Display node column in Pod list - Display node column in Pod list
- Unify age column output with kubectl - Unify age column output with kubectl
@ -16,6 +19,8 @@ Here you can find description of changes we've built into each release. While we
- Improve Pod tolerations layout - Improve Pod tolerations layout
- Lens metrics: scrape only lens-metrics namespace - Lens metrics: scrape only lens-metrics namespace
- Lens metrics: Prometheus v2.19.3 - Lens metrics: Prometheus v2.19.3
- Update bundled kubectl to v1.18.15
- Improve how watch requests are handled
- Export PodDetailsList component to extension API - Export PodDetailsList component to extension API
- Export Wizard components to extension API - Export Wizard components to extension API
- Export NamespaceSelect component to extension API - Export NamespaceSelect component to extension API