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

Merge branch 'master' into fix/consistent-inputs

This commit is contained in:
Alex Andreev 2022-02-03 14:16:16 +03:00
commit fdf7011d20
20 changed files with 297 additions and 98 deletions

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.3.0", "version": "5.4.0-alpha.1",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -304,7 +304,7 @@
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2", "@types/react-router-dom": "^5.3.2",
"@types/react-select": "3.1.2", "@types/react-select": "3.1.2",
"@types/react-table": "^7.7.8", "@types/react-table": "^7.7.9",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/readable-stream": "^2.3.12", "@types/readable-stream": "^2.3.12",

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Node } from "../endpoints";
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
describe("Nodes tests", () => {
describe("getRoleLabels()", () => {
it("should return empty string if labels is not present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
},
});
expect(node.getRoleLabels()).toBe("");
});
it("should return empty string if labels is empty object", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {},
},
});
expect(node.getRoleLabels()).toBe("");
});
it("should return rest of keys with substring node-role.kubernetes.io/", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io/foobar1": "bat",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, foobar1");
});
it("should return rest of keys with substring node-role.kubernetes.io/ after last /", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io//////foobar1": "bat",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, foobar1");
});
it("should return value of label kubernetes.io/role if present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"kubernetes.io/role": "master",
},
},
});
expect(node.getRoleLabels()).toBe("master");
});
it("should return value of label node.kubernetes.io/role if present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node.kubernetes.io/role": "master",
},
},
});
expect(node.getRoleLabels()).toBe("master");
});
it("all sources should be joined together", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"aksjhdkjahsdnode-role.kubernetes.io/foobar": "bat",
"kubernetes.io/role": "master",
"node.kubernetes.io/role": "master-v2-max",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, master, master-v2-max");
});
});
});

View File

@ -140,6 +140,12 @@ function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | It
} }
} }
/**
* This regex is used in the `getRoleLabels()` method bellow, but placed here
* as factoring out regexes is best practice.
*/
const nodeRoleLabelKeyMatcher = /^.*node-role.kubernetes.io\/+(?<role>.+)$/;
export class Node extends KubeObject { export class Node extends KubeObject {
static kind = "Node"; static kind = "Node";
static namespaced = false; static namespaced = false;
@ -165,17 +171,29 @@ export class Node extends KubeObject {
return this.spec.taints || []; return this.spec.taints || [];
} }
getRoleLabels() { getRoleLabels(): string {
if (!this.metadata?.labels || typeof this.metadata.labels !== "object") { const { labels } = this.metadata;
if (!labels || typeof labels !== "object") {
return ""; return "";
} }
const roleLabels = Object.keys(this.metadata.labels) const roleLabels: string[] = [];
.filter(key => key.includes("node-role.kubernetes.io"))
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
if (this.metadata.labels["kubernetes.io/role"] != undefined) { for (const labelKey of Object.keys(labels)) {
roleLabels.push(this.metadata.labels["kubernetes.io/role"]); const match = nodeRoleLabelKeyMatcher.exec(labelKey);
if (match) {
roleLabels.push(match.groups.role);
}
}
if (typeof labels["kubernetes.io/role"] === "string") {
roleLabels.push(labels["kubernetes.io/role"]);
}
if (typeof labels["node.kubernetes.io/role"] === "string") {
roleLabels.push(labels["node.kubernetes.io/role"]);
} }
return roleLabels.join(", "); return roleLabels.join(", ");

View File

@ -179,12 +179,13 @@ export async function getHistory(name: string, namespace: string, kubeconfigPath
} }
export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) { export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) {
return JSON.parse(await execHelm([ await execHelm([
"rollback", "rollback",
name, name,
`${revision}`,
"--namespace", namespace, "--namespace", namespace,
"--kubeconfig", kubeconfigPath, "--kubeconfig", kubeconfigPath,
])); ]);
} }
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) { async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {

View File

@ -102,9 +102,7 @@ class HelmService {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Rollback release"); logger.debug("Rollback release");
const output = rollback(releaseName, namespace, revision, proxyKubeconfig); await rollback(releaseName, namespace, revision, proxyKubeconfig);
return { message: output };
} }
} }

View File

@ -71,9 +71,9 @@ export class HelmApiRoute {
const { cluster, params, payload, response } = request; const { cluster, params, payload, response } = request;
try { try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision); await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
respondJson(response, result); response.end();
} catch (error) { } catch (error) {
logger.debug(error); logger.debug(error);
respondText(response, error?.toString() || "Error rolling back chart", 422); respondText(response, error?.toString() || "Error rolling back chart", 422);

View File

@ -94,6 +94,7 @@ export class NodeShellSession extends ShellSession {
tolerations: [{ tolerations: [{
operator: "Exists", operator: "Exists",
}], }],
priorityClassName: "system-node-critical",
containers: [{ containers: [{
name: "shell", name: "shell",
image: this.cluster.nodeShellImage, image: this.cluster.nodeShellImage,

View File

@ -10,7 +10,7 @@ import releaseInjectable from "./release.injectable";
const releaseDetailsInjectable = getInjectable({ const releaseDetailsInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
asyncComputed(async () => { asyncComputed(async () => {
const release = di.inject(releaseInjectable).value.get(); const release = di.inject(releaseInjectable).get();
return await getRelease(release.name, release.namespace); return await getRelease(release.name, release.namespace);
}), }),

View File

@ -7,7 +7,7 @@ import "./release-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import { computed, makeObservable, observable } from "mobx"; import { computed, IComputedValue, makeObservable, observable } 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 type { HelmRelease, IReleaseDetails, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import type { HelmRelease, IReleaseDetails, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
@ -39,7 +39,7 @@ interface Props {
} }
interface Dependencies { interface Dependencies {
release: IAsyncComputed<HelmRelease> release: IComputedValue<HelmRelease>
releaseDetails: IAsyncComputed<IReleaseDetails> releaseDetails: IAsyncComputed<IReleaseDetails>
releaseValues: IAsyncComputed<string> releaseValues: IAsyncComputed<string>
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails> updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>
@ -59,7 +59,7 @@ class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
} }
@computed get release() { @computed get release() {
return this.props.release.value.get(); return this.props.release.get();
} }
@computed get details() { @computed get details() {

View File

@ -12,7 +12,13 @@ import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-sho
const releaseValuesInjectable = getInjectable({ const releaseValuesInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
asyncComputed(async () => { asyncComputed(async () => {
const release = di.inject(releaseInjectable).value.get(); const release = di.inject(releaseInjectable).get();
// TODO: Figure out way to get rid of defensive code
if (!release) {
return "";
}
const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value; const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value;
try { try {

View File

@ -6,14 +6,14 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { matches } from "lodash/fp"; import { matches } from "lodash/fp";
import releasesInjectable from "../releases.injectable"; import releasesInjectable from "../releases.injectable";
import releaseRouteParametersInjectable from "./release-route-parameters.injectable"; import releaseRouteParametersInjectable from "./release-route-parameters.injectable";
import { asyncComputed } from "@ogre-tools/injectable-react"; import { computed } from "mobx";
const releaseInjectable = getInjectable({ const releaseInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const releases = di.inject(releasesInjectable); const releases = di.inject(releasesInjectable);
const releaseRouteParameters = di.inject(releaseRouteParametersInjectable); const releaseRouteParameters = di.inject(releaseRouteParametersInjectable);
return asyncComputed(async () => { return computed(() => {
const { name, namespace } = releaseRouteParameters.get(); const { name, namespace } = releaseRouteParameters.get();
if (!name || !namespace) { if (!name || !namespace) {

View File

@ -3,10 +3,29 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { updateRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import {
IReleaseUpdatePayload,
updateRelease,
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
import releasesInjectable from "../releases.injectable";
const updateReleaseInjectable = getInjectable({ const updateReleaseInjectable = getInjectable({
instantiate: () => updateRelease, instantiate: (di) => {
const releases = di.inject(releasesInjectable);
return async (
name: string,
namespace: string,
payload: IReleaseUpdatePayload,
) => {
const result = await updateRelease(name, namespace, payload);
releases.invalidate();
return result;
};
},
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -43,6 +43,8 @@
} }
.pinIcon { .pinIcon {
--color-active: var(--textColorAccent);
transition: none; transition: none;
opacity: 0; opacity: 0;
margin-left: var(--padding); margin-left: var(--padding);

View File

@ -9,19 +9,23 @@
} }
.quota-entries { .quota-entries {
margin: -$margin * 0.5; display: flex;
gap: 8px;
margin-left: -1px;
flex-wrap: wrap;
&:empty { &:empty {
display: none; display: none;
} }
.quota { .quota {
--flex-gap: #{$padding};
border: 1px solid var(--halfGray); border: 1px solid var(--halfGray);
border-radius: $radius; border-radius: $radius;
margin: $margin * 0.5;
padding: $padding * 0.5 $padding; padding: $padding * 0.5 $padding;
transition: all 150ms ease; transition: all 150ms ease;
display: flex;
align-items: center;
gap: 8px;
&:hover { &:hover {
box-shadow: inset 0 0 0 1px var(--borderColor); box-shadow: inset 0 0 0 1px var(--borderColor);

View File

@ -124,7 +124,7 @@ export class Events extends React.Component<Props> {
}; };
render() { render() {
const { store, visibleItems } = this; const { store } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props; const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = ( const events = (
@ -137,7 +137,7 @@ export class Events extends React.Component<Props> {
renderHeaderTitle="Events" renderHeaderTitle="Events"
customizeHeader={this.customizeHeader} customizeHeader={this.customizeHeader}
isSelectable={false} isSelectable={false}
items={visibleItems} getItems={() => this.visibleItems}
virtual={!compact} virtual={!compact}
tableProps={{ tableProps={{
sortSyncWithUrl: false, sortSyncWithUrl: false,

View File

@ -136,8 +136,8 @@ export class NonInjectedUpgradeChart extends React.Component<Props & Dependencie
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this; const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
const { className } = this.props; const { className } = this.props;
if (!release || this.props.upgradeChartTabStore.isReady(tabId) || !version) { if (!release || !this.props.upgradeChartTabStore.isReady(tabId) || !version) {
return <Spinner center/>; return <Spinner center />;
} }
const currentVersion = release.getVersion(); const currentVersion = release.getVersion();
const controlsAndInfo = ( const controlsAndInfo = (

View File

@ -111,7 +111,7 @@
} }
&.active { &.active {
color: var(--textColorAccent); color: var(--color-active);
box-shadow: 0 0 0 2px var(--iconActiveBackground); box-shadow: 0 0 0 2px var(--iconActiveBackground);
background-color: var(--iconActiveBackground); background-color: var(--iconActiveBackground);
} }

View File

@ -7,7 +7,7 @@ import "./item-list-layout.scss";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { computed, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { Observer, observer } from "mobx-react";
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table"; import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table";
import { boundMethod, cssNames, IClassName, isReactNode, prevDefault, stopPropagation } from "../../utils"; import { boundMethod, cssNames, IClassName, isReactNode, prevDefault, stopPropagation } from "../../utils";
@ -70,58 +70,68 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
@boundMethod @boundMethod
getRow(uid: string) { getRow(uid: string) {
const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
store, hasDetailsView, onDetails,
copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
} = this.props;
const { isSelected } = store;
const item = this.props.getItems().find(item => item.getId() == uid);
if (!item) return null;
const itemId = item.getId();
return ( return (
<TableRow <div key={uid}>
key={itemId} <Observer>
nowrap {() => {
searchItem={item} const {
sortItem={item} isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
selected={detailsItem && detailsItem.getId() === itemId} store, hasDetailsView, onDetails,
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined} copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
{...customizeTableRowProps(item)} } = this.props;
> const { isSelected } = store;
{isSelectable && ( const item = this.props.getItems().find(item => item.getId() == uid);
<TableCell
checkbox
isChecked={isSelected(item)}
onClick={prevDefault(() => store.toggleSelection(item))}
/>
)}
{
renderTableContents(item).map((content, index) => {
const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
const headCell = renderTableHeader?.[index];
if (copyClassNameFromHeadCells && headCell) { if (!item) return null;
cellProps.className = cssNames(cellProps.className, headCell.className); const itemId = item.getId();
}
if (!headCell || this.showColumn(headCell)) { return (
return <TableCell key={index} {...cellProps} />; <TableRow
} nowrap
searchItem={item}
sortItem={item}
selected={detailsItem && detailsItem.getId() === itemId}
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
{...customizeTableRowProps(item)}
>
{isSelectable && (
<TableCell
checkbox
isChecked={isSelected(item)}
onClick={prevDefault(() => store.toggleSelection(item))}
/>
)}
{renderTableContents(item).map((content, index) => {
const cellProps: TableCellProps = isReactNode(content)
? { children: content }
: content;
const headCell = renderTableHeader?.[index];
return null; if (copyClassNameFromHeadCells && headCell) {
}) cellProps.className = cssNames(
} cellProps.className,
{renderItemMenu && ( headCell.className,
<TableCell className="menu"> );
<div onClick={stopPropagation}> }
{renderItemMenu(item, store)}
</div> if (!headCell || this.showColumn(headCell)) {
</TableCell> return <TableCell key={index} {...cellProps} />;
)} }
</TableRow>
return null;
})}
{renderItemMenu && (
<TableCell className="menu">
<div onClick={stopPropagation}>
{renderItemMenu(item, store)}
</div>
</TableCell>
)}
</TableRow>
);
}}
</Observer>
</div>
); );
} }
@ -190,11 +200,15 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
return ( return (
<TableHead showTopLine nowrap> <TableHead showTopLine nowrap>
{isSelectable && ( {isSelectable && (
<TableCell <Observer>
checkbox {() => (
isChecked={store.isSelectedAll(enabledItems)} <TableCell
onClick={prevDefault(() => store.toggleSelectionAll(enabledItems))} checkbox
/> isChecked={store.isSelectedAll(enabledItems)}
onClick={prevDefault(() => store.toggleSelectionAll(enabledItems))}
/>
)}
</Observer>
)} )}
{renderTableHeader.map((cellProps, index) => ( {renderTableHeader.map((cellProps, index) => (
this.showColumn(cellProps) && ( this.showColumn(cellProps) && (
@ -213,7 +227,6 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, detailsItem, className, tableProps = {}, tableId,
} = this.props; } = this.props;
const { selectedItems } = store;
const selectedItemId = detailsItem && detailsItem.getId(); const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type);
@ -234,11 +247,18 @@ export class ItemListLayoutContent<I extends ItemObject> extends React.Component
{this.renderTableHeader()} {this.renderTableHeader()}
{this.renderItems()} {this.renderItems()}
</Table> </Table>
<AddRemoveButtons
onRemove={selectedItems.length ? this.removeItemsDialog : null} <Observer>
removeTooltip={`Remove selected items (${selectedItems.length})`} {() => (
{...addRemoveButtons} <AddRemoveButtons
/> onRemove={
store.selectedItems.length ? this.removeItemsDialog : null
}
removeTooltip={`Remove selected items (${store.selectedItems.length})`}
{...addRemoveButtons}
/>
)}
</Observer>
</div> </div>
); );
} }

View File

@ -46,7 +46,6 @@ export function hideDetails() {
} }
export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) { export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) {
console.debug("getDetailsUrl", { selfLink, resetSelected, mergeGlobals });
const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : ""); const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : "");
params.set(kubeDetailsUrlParam.name, selfLink); params.set(kubeDetailsUrlParam.name, selfLink);

View File

@ -1762,10 +1762,10 @@
"@types/react-dom" "*" "@types/react-dom" "*"
"@types/react-transition-group" "*" "@types/react-transition-group" "*"
"@types/react-table@^7.7.8": "@types/react-table@^7.7.9":
version "7.7.8" version "7.7.9"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.8.tgz#b1aa5fb7a54432969262d2306b87fdbb9a5ee647" resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.9.tgz#ea82875775fc6ee71a28408dcc039396ae067c92"
integrity sha512-OMhbPlf+uUGte3M1WdArEKeBkyQ1XJxKvFYs+o1dGGGyaSVIqxPPQmBZ6Skkw0V9y0F/kOY7rnTD8r9GbfpBOg== integrity sha512-ejP/J20Zlj9VmuLh73YgYkW2xOSFTW39G43rPH93M4mYWdMmqv66lCCr+axZpkdtlNLGjvMG2CwzT4S6abaeGQ==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"