mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
commit
9494933e4a
@ -74,6 +74,7 @@ export class Tracker extends Util.Singleton {
|
||||
}
|
||||
|
||||
reportPeriodically() {
|
||||
this.reportData();
|
||||
this.reportInterval = setInterval(() => {
|
||||
this.reportData();
|
||||
}, 60 * 60 * 1000); // report every 1h
|
||||
|
||||
@ -226,7 +226,7 @@ describe("Lens integration tests", () => {
|
||||
pages: [{
|
||||
name: "Cluster",
|
||||
href: "cluster",
|
||||
expectedSelector: "div.Cluster div.label",
|
||||
expectedSelector: "div.ClusterOverview div.label",
|
||||
expectedText: "Master"
|
||||
}]
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
|
||||
@ -18,7 +18,7 @@ const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
it("emits add for added extension", async done => {
|
||||
globalThis.__non_webpack_require__.mockImplementationOnce(() => ({
|
||||
globalThis.__non_webpack_require__.mockImplementation(() => ({
|
||||
name: "my-extension"
|
||||
}));
|
||||
let addHandler: (filePath: string) => void;
|
||||
@ -61,9 +61,6 @@ describe("ExtensionDiscovery", () => {
|
||||
});
|
||||
|
||||
it("doesn't emit add for added file under extension", async done => {
|
||||
globalThis.__non_webpack_require__.mockImplementationOnce(() => ({
|
||||
name: "my-extension"
|
||||
}));
|
||||
let addHandler: (filePath: string) => void;
|
||||
|
||||
const mockWatchInstance: any = {
|
||||
|
||||
@ -155,23 +155,26 @@ export class ExtensionDiscovery {
|
||||
.on("unlinkDir", this.handleWatchUnlinkDir);
|
||||
}
|
||||
|
||||
handleWatchFileAdd = async (filePath: string) => {
|
||||
handleWatchFileAdd = async (manifestPath: string) => {
|
||||
// e.g. "foo/package.json"
|
||||
const relativePath = path.relative(this.localFolderPath, filePath);
|
||||
const relativePath = path.relative(this.localFolderPath, manifestPath);
|
||||
|
||||
// Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies
|
||||
// that the added file is in a folder under local folder path.
|
||||
// This safeguards against a file watch being triggered under a sub-directory which is not an extension.
|
||||
const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2;
|
||||
|
||||
if (path.basename(filePath) === manifestFilename && isUnderLocalFolderPath) {
|
||||
if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
||||
try {
|
||||
const absPath = path.dirname(filePath);
|
||||
const absPath = path.dirname(manifestPath);
|
||||
|
||||
// this.loadExtensionFromPath updates this.packagesJson
|
||||
const extension = await this.loadExtensionFromPath(absPath);
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
|
||||
if (extension) {
|
||||
// Remove a broken symlink left by a previous installation if it exists.
|
||||
await this.removeSymlinkByManifestPath(manifestPath);
|
||||
|
||||
// Install dependencies for the new extension
|
||||
await this.installPackages();
|
||||
|
||||
@ -199,6 +202,9 @@ export class ExtensionDiscovery {
|
||||
.find(([, extensionFolder]) => filePath === extensionFolder)?.[0];
|
||||
|
||||
if (extensionName !== undefined) {
|
||||
// If the extension is deleted manually while the application is running, also remove the symlink
|
||||
await this.removeSymlinkByPackageName(extensionName);
|
||||
|
||||
delete this.packagesJson.dependencies[extensionName];
|
||||
|
||||
// Reinstall dependencies to remove the extension from package.json
|
||||
@ -216,6 +222,26 @@ export class ExtensionDiscovery {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the symlink under node_modules if exists.
|
||||
* If we don't remove the symlink, the uninstall would leave a non-working symlink,
|
||||
* which wouldn't be fixed if the extension was reinstalled, causing the extension not to work.
|
||||
* @param name e.g. "@mirantis/lens-extension-cc"
|
||||
*/
|
||||
removeSymlinkByPackageName(name: string) {
|
||||
return fs.remove(this.getInstalledPath(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the symlink under node_modules if it exists.
|
||||
* @param manifestPath Path to package.json
|
||||
*/
|
||||
removeSymlinkByManifestPath(manifestPath: string) {
|
||||
const manifestJson = __non_webpack_require__(manifestPath);
|
||||
|
||||
return this.removeSymlinkByPackageName(manifestJson.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls extension.
|
||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||
@ -224,16 +250,7 @@ export class ExtensionDiscovery {
|
||||
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
|
||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||
|
||||
// remove the symlink under node_modules.
|
||||
// If we don't remove the symlink, the uninstall would leave a non-working symlink,
|
||||
// which wouldn't be fixed if the extension was reinstalled, causing the extension not to work.
|
||||
await fs.remove(this.getInstalledPath(manifest.name));
|
||||
|
||||
const exists = await fs.pathExists(absolutePath);
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(`Extension path ${absolutePath} doesn't exist`);
|
||||
}
|
||||
await this.removeSymlinkByPackageName(manifest.name);
|
||||
|
||||
// fs.remove does nothing if the path doesn't exist anymore
|
||||
await fs.remove(absolutePath);
|
||||
@ -290,6 +307,10 @@ export class ExtensionDiscovery {
|
||||
return path.join(this.getInstalledPath(name), manifestFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns InstalledExtension from path to package.json file.
|
||||
* Also updates this.packagesJson.
|
||||
*/
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false }: {
|
||||
isBundled?: boolean;
|
||||
} = {}): Promise<InstalledExtension | null> {
|
||||
@ -349,7 +370,7 @@ export class ExtensionDiscovery {
|
||||
}
|
||||
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const extension = await this.loadExtensionFromPath(absPath, { isBundled: true });
|
||||
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
|
||||
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
@ -384,7 +405,7 @@ export class ExtensionDiscovery {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extension = await this.loadExtensionFromPath(absPath);
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
@ -398,8 +419,9 @@ export class ExtensionDiscovery {
|
||||
|
||||
/**
|
||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||
* @param absPath Folder path to extension
|
||||
*/
|
||||
async loadExtensionFromPath(absPath: string, { isBundled = false }: {
|
||||
async loadExtensionFromFolder(absPath: string, { isBundled = false }: {
|
||||
isBundled?: boolean;
|
||||
} = {}): Promise<InstalledExtension | null> {
|
||||
const manifestPath = path.resolve(absPath, manifestFilename);
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
.ClusterIssues {
|
||||
min-height: 350px;
|
||||
position: relative;
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
|
||||
@include media("<1024px") {
|
||||
grid-column-start: 1!important;
|
||||
grid-column-end: 1!important;
|
||||
}
|
||||
|
||||
&.wide {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
.SubHeader {
|
||||
.Icon {
|
||||
font-size: 130%;
|
||||
|
||||
@ -6,10 +6,10 @@ import { observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Radio, RadioGroup } from "../radio";
|
||||
import { clusterStore, MetricNodeRole, MetricType } from "./cluster.store";
|
||||
import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
|
||||
|
||||
export const ClusterMetricSwitchers = observer(() => {
|
||||
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterStore;
|
||||
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const metricsValues = getMetricsValues(metrics);
|
||||
const disableRoles = !masterNodes.length || !workerNodes.length;
|
||||
@ -22,7 +22,7 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
||||
value={metricNodeRole}
|
||||
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric}
|
||||
onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric}
|
||||
>
|
||||
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
|
||||
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
|
||||
@ -33,7 +33,7 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
||||
value={metricType}
|
||||
onChange={(value: MetricType) => clusterStore.metricType = value}
|
||||
onChange={(value: MetricType) => clusterOverviewStore.metricType = value}
|
||||
>
|
||||
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
|
||||
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
|
||||
|
||||
@ -3,7 +3,7 @@ import "./cluster-metrics.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChartOptions, ChartPoint } from "chart.js";
|
||||
import { clusterStore, MetricType } from "./cluster.store";
|
||||
import { clusterOverviewStore, MetricType } from "./cluster-overview.store";
|
||||
import { BarChart } from "../chart";
|
||||
import { bytesToUnits } from "../../utils";
|
||||
import { Spinner } from "../spinner";
|
||||
@ -13,10 +13,9 @@ import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
|
||||
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
||||
|
||||
export const ClusterMetrics = observer(() => {
|
||||
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics, liveMetrics } = clusterStore;
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterStore.metrics);
|
||||
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
const metricValues = getMetricsValues(metrics);
|
||||
const liveMetricValues = getMetricsValues(liveMetrics);
|
||||
const colors = { cpu: "#3D90CE", memory: "#C93DCE" };
|
||||
const data = metricValues.map(value => ({
|
||||
x: value[0],
|
||||
@ -70,7 +69,7 @@ export const ClusterMetrics = observer(() => {
|
||||
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
|
||||
|
||||
const renderMetrics = () => {
|
||||
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
|
||||
if (!metricValues.length && !metricsLoaded) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.Cluster {
|
||||
.ClusterOverview {
|
||||
$gridGap: $margin * 2;
|
||||
|
||||
position: relative;
|
||||
@ -1,4 +1,4 @@
|
||||
import { observable, reaction, when } from "mobx";
|
||||
import { action, observable, reaction, when } from "mobx";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
@ -17,11 +17,10 @@ export enum MetricNodeRole {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster> {
|
||||
api = clusterApi;
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
@observable liveMetrics: Partial<IClusterMetrics> = {};
|
||||
@observable metricsLoaded = false;
|
||||
@observable metricType: MetricType;
|
||||
@observable metricNodeRole: MetricNodeRole;
|
||||
@ -46,9 +45,8 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
reaction(() => this.metricNodeRole, () => {
|
||||
if (!this.metricsLoaded) return;
|
||||
this.metrics = {};
|
||||
this.liveMetrics = {};
|
||||
this.metricsLoaded = false;
|
||||
this.getAllMetrics();
|
||||
this.loadMetrics();
|
||||
});
|
||||
|
||||
// check which node type to select
|
||||
@ -60,33 +58,16 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async loadMetrics(params?: IMetricsReqParams) {
|
||||
await when(() => nodesStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
||||
|
||||
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
|
||||
}
|
||||
|
||||
async getAllMetrics() {
|
||||
await this.getMetrics();
|
||||
await this.getLiveMetrics();
|
||||
this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params);
|
||||
this.metricsLoaded = true;
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
this.metrics = await this.loadMetrics();
|
||||
}
|
||||
|
||||
async getLiveMetrics() {
|
||||
const step = 3;
|
||||
const range = 15;
|
||||
const end = Date.now() / 1000;
|
||||
const start = end - range;
|
||||
|
||||
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
|
||||
}
|
||||
|
||||
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
|
||||
switch (this.metricType) {
|
||||
case MetricType.CPU:
|
||||
@ -111,5 +92,5 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterStore = new ClusterStore();
|
||||
apiManager.registerStore(clusterStore);
|
||||
export const clusterOverviewStore = new ClusterOverviewStore();
|
||||
apiManager.registerStore(clusterOverviewStore);
|
||||
79
src/renderer/components/+cluster/cluster-overview.tsx
Normal file
79
src/renderer/components/+cluster/cluster-overview.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import "./cluster-overview.scss";
|
||||
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import { clusterOverviewStore } from "./cluster-overview.store";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
|
||||
@observer
|
||||
export class ClusterOverview extends React.Component {
|
||||
private stores: KubeObjectStore<any>[] = [];
|
||||
private subscribers: Array<() => void> = [];
|
||||
private metricPoller = interval(60, this.loadMetrics);
|
||||
|
||||
@disposeOnUnmount
|
||||
fetchMetrics = reaction(
|
||||
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||
() => this.metricPoller.restart(true)
|
||||
);
|
||||
|
||||
loadMetrics() {
|
||||
getHostedCluster().available && clusterOverviewStore.loadMetrics();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (isAllowedResource("nodes")) {
|
||||
this.stores.push(nodesStore);
|
||||
}
|
||||
|
||||
if (isAllowedResource("pods")) {
|
||||
this.stores.push(podsStore);
|
||||
}
|
||||
|
||||
if (isAllowedResource("events")) {
|
||||
this.stores.push(eventStore);
|
||||
}
|
||||
|
||||
await Promise.all(this.stores.map(store => store.loadAll()));
|
||||
this.loadMetrics();
|
||||
|
||||
this.subscribers = this.stores.map(store => store.subscribe());
|
||||
this.metricPoller.start();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscribers.forEach(dispose => dispose()); // unsubscribe all
|
||||
this.metricPoller.stop();
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<div className="ClusterOverview">
|
||||
{!isLoaded ? <Spinner center/> : (
|
||||
<>
|
||||
<ClusterMetrics/>
|
||||
<ClusterPieCharts/>
|
||||
<ClusterIssues/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { clusterStore, MetricNodeRole } from "./cluster.store";
|
||||
import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Icon } from "../icon";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
@ -27,7 +27,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
};
|
||||
|
||||
const renderCharts = () => {
|
||||
const data = getMetricLastPoints(clusterStore.metrics);
|
||||
const data = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
|
||||
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
|
||||
const { podUsage, podCapacity } = data;
|
||||
@ -173,7 +173,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
|
||||
const renderContent = () => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const { metricNodeRole, metricsLoaded } = clusterStore;
|
||||
const { metricNodeRole, metricsLoaded } = clusterOverviewStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
|
||||
if (!nodes.length) {
|
||||
@ -192,7 +192,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>;
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
import "./cluster.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { Spinner } from "../spinner";
|
||||
import { cssNames, interval, isElectron } from "../../utils";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { clusterStore } from "./cluster.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
|
||||
@observer
|
||||
export class Cluster extends React.Component {
|
||||
private dependentStores = [nodesStore, podsStore];
|
||||
|
||||
private watchers = [
|
||||
interval(60, () => { getHostedCluster().available && clusterStore.getMetrics();}),
|
||||
interval(20, () => { getHostedCluster().available && eventStore.loadAll();})
|
||||
];
|
||||
|
||||
@computed get isLoaded() {
|
||||
return nodesStore.isLoaded && podsStore.isLoaded;
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
async componentDidMount() {
|
||||
const { dependentStores } = this;
|
||||
|
||||
if (!isAllowedResource("nodes")) {
|
||||
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
|
||||
}
|
||||
this.watchers.forEach(watcher => watcher.start(true));
|
||||
|
||||
await Promise.all([
|
||||
...dependentStores.map(store => store.loadAll()),
|
||||
clusterStore.getAllMetrics()
|
||||
]);
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
...dependentStores.map(store => store.subscribe()),
|
||||
() => this.watchers.forEach(watcher => watcher.stop()),
|
||||
reaction(
|
||||
() => clusterStore.metricNodeRole,
|
||||
() => this.watchers.forEach(watcher => watcher.restart())
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoaded } = this;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<div className="Cluster">
|
||||
{!isLoaded && <Spinner center/>}
|
||||
{isLoaded && (
|
||||
<>
|
||||
<ClusterMetrics/>
|
||||
<ClusterPieCharts/>
|
||||
<ClusterIssues className={cssNames({ wide: isElectron })}/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -134,7 +134,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "HorizontalPodAutoscaler",
|
||||
apiVersions: ["autoscaling/v1"],
|
||||
apiVersions: ["autoscaling/v2beta1"],
|
||||
components: {
|
||||
Details: (props) => <HpaDetails {...props} />
|
||||
}
|
||||
@ -142,7 +142,7 @@ kubeObjectDetailRegistry.add({
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "HorizontalPodAutoscaler",
|
||||
apiVersions: ["autoscaling/v1"],
|
||||
apiVersions: ["autoscaling/v2beta1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
|
||||
@ -26,9 +26,11 @@ export const NodeCharts = observer(() => {
|
||||
const [
|
||||
memoryUsage,
|
||||
memoryRequests,
|
||||
_memoryLimits, // eslint-disable-line unused-imports/no-unused-vars-ts
|
||||
memoryCapacity,
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
_cpuLimits, // eslint-disable-line unused-imports/no-unused-vars-ts
|
||||
cpuCapacity,
|
||||
podUsage,
|
||||
podCapacity,
|
||||
|
||||
@ -91,14 +91,14 @@ export class CronJobDetails extends React.Component<Props> {
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "CronJob",
|
||||
apiVersions: ["batch/v1"],
|
||||
apiVersions: ["batch/v1beta1"],
|
||||
components: {
|
||||
Details: (props) => <CronJobDetails {...props} />
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "CronJob",
|
||||
apiVersions: ["batch/v1"],
|
||||
apiVersions: ["batch/v1beta1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
|
||||
@ -128,33 +128,45 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
const initReplicas = 1;
|
||||
|
||||
deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
|
||||
const { getByTestId } = render(<DeploymentScaleDialog />);
|
||||
const component = render(<DeploymentScaleDialog />);
|
||||
|
||||
DeploymentScaleDialog.open(dummyDeployment);
|
||||
await waitFor(async () => {
|
||||
const desiredScale = await getByTestId("desired-scale");
|
||||
|
||||
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
|
||||
expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`);
|
||||
expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`);
|
||||
expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`);
|
||||
});
|
||||
const up = await getByTestId("desired-replicas-up");
|
||||
const down = await getByTestId("desired-replicas-down");
|
||||
const up = await component.getByTestId("desired-replicas-up");
|
||||
const down = await component.getByTestId("desired-replicas-down");
|
||||
|
||||
fireEvent.click(up);
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`);
|
||||
fireEvent.click(down);
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent("1");
|
||||
// edge case, desiredScale must > 0
|
||||
fireEvent.click(down);
|
||||
fireEvent.click(down);
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent("1");
|
||||
const times = 120;
|
||||
expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`);
|
||||
expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`);
|
||||
expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas + 1}`);
|
||||
|
||||
fireEvent.click(down);
|
||||
expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`);
|
||||
expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`);
|
||||
expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`);
|
||||
|
||||
// edge case, desiredScale must = 0
|
||||
let times = 10;
|
||||
|
||||
for (let i = 0; i < times; i++) {
|
||||
fireEvent.click(down);
|
||||
}
|
||||
expect(await component.getByTestId("desired-scale")).toHaveTextContent("0");
|
||||
expect((await component.baseElement.querySelector("input").value)).toBe("0");
|
||||
|
||||
// edge case, desiredScale must = 100 scaleMax (100)
|
||||
times = 120;
|
||||
|
||||
// edge case, desiredScale must < scaleMax (100)
|
||||
for (let i = 0; i < times; i++) {
|
||||
fireEvent.click(up);
|
||||
}
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent("100");
|
||||
expect(await component.getByTestId("desired-scale")).toHaveTextContent("100");
|
||||
expect((component.baseElement.querySelector("input").value)).toBe("100");
|
||||
expect(await component.getByTestId("warning"))
|
||||
.toHaveTextContent("High number of replicas may cause cluster performance issues");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
};
|
||||
|
||||
desiredReplicasDown = () => {
|
||||
this.desiredReplicas > 1 && this.desiredReplicas--;
|
||||
this.desiredReplicas > 0 && this.desiredReplicas--;
|
||||
};
|
||||
|
||||
renderContents() {
|
||||
@ -124,7 +124,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
{warning &&
|
||||
<div className="warning">
|
||||
<div className="warning" data-testid="warning">
|
||||
<Icon material="warning"/>
|
||||
<Trans>High number of replicas may cause cluster performance issues</Trans>
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,7 @@ import { Workloads, workloadsRoute, workloadsURL } from "./+workloads";
|
||||
import { Namespaces, namespacesRoute } from "./+namespaces";
|
||||
import { Network, networkRoute } from "./+network";
|
||||
import { Storage, storageRoute } from "./+storage";
|
||||
import { Cluster } from "./+cluster/cluster";
|
||||
import { ClusterOverview } from "./+cluster/cluster-overview";
|
||||
import { Config, configRoute } from "./+config";
|
||||
import { Events } from "./+events/events";
|
||||
import { eventRoute } from "./+events";
|
||||
@ -180,7 +180,7 @@ export class App extends React.Component {
|
||||
<ErrorBoundary>
|
||||
<MainLayout>
|
||||
<Switch>
|
||||
<Route component={Cluster} {...clusterRoute}/>
|
||||
<Route component={ClusterOverview} {...clusterRoute}/>
|
||||
<Route component={Nodes} {...nodesRoute}/>
|
||||
<Route component={Workloads} {...workloadsRoute}/>
|
||||
<Route component={Config} {...configRoute}/>
|
||||
|
||||
@ -95,15 +95,9 @@ export class PodLogList extends React.Component<Props> {
|
||||
@action
|
||||
setLastLineVisibility = (props: ListOnScrollProps) => {
|
||||
const { scrollHeight, clientHeight } = this.virtualListDiv.current;
|
||||
const { scrollOffset, scrollDirection } = props;
|
||||
const { scrollOffset } = props;
|
||||
|
||||
if (scrollDirection == "backward") {
|
||||
this.isLastLineVisible = false;
|
||||
} else {
|
||||
if (clientHeight + scrollOffset === scrollHeight) {
|
||||
this.isLastLineVisible = true;
|
||||
}
|
||||
}
|
||||
this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -2,7 +2,19 @@
|
||||
|
||||
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.0.1 (current version)
|
||||
## 4.0.2 (current version)
|
||||
|
||||
We are aware some users are encountering issues and regressions from previous version. Many of these issues are something we have not seen as part of our automated or manual testing process. To make it worse, some of them are really difficult to reproduce. We want to ensure we are putting all our energy and effort trying to resolve these issues. We hope you are patient. Expect to see new patch releases still in the coming days! Fixes in this version:
|
||||
|
||||
- Fix: use correct apiversion for HPA details
|
||||
- Fix: use correct apiversion fro CronJob details
|
||||
- Fix: wrong values in node metrics
|
||||
- Fix: Deployment scale button "minus"
|
||||
- Fix: remove symlink on extension install and manual runtime uninstall
|
||||
- Fix: logs autoscroll behaviour
|
||||
- Performance fixes
|
||||
|
||||
## 4.0.1
|
||||
|
||||
- Extension install/uninstall fixes
|
||||
- Fix status brick styles in pod-menu-extension
|
||||
|
||||
Loading…
Reference in New Issue
Block a user