mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into fix/hotbar-stale-items
This commit is contained in:
commit
3fcd44b676
@ -207,6 +207,21 @@ describe("HotbarStore", () => {
|
|||||||
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
|
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("new items takes first empty cell", () => {
|
||||||
|
const hotbarStore = HotbarStore.createInstance();
|
||||||
|
const test = new CatalogEntityItem(testCluster);
|
||||||
|
const minikube = new CatalogEntityItem(minikubeCluster);
|
||||||
|
const aws = new CatalogEntityItem(awsCluster);
|
||||||
|
|
||||||
|
hotbarStore.load();
|
||||||
|
hotbarStore.addToHotbar(test);
|
||||||
|
hotbarStore.addToHotbar(aws);
|
||||||
|
hotbarStore.restackItems(0, 3);
|
||||||
|
hotbarStore.addToHotbar(minikube);
|
||||||
|
|
||||||
|
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
|
||||||
|
});
|
||||||
|
|
||||||
it("throws if invalid arguments provided", () => {
|
it("throws if invalid arguments provided", () => {
|
||||||
// Prevent writing to stderr during this render.
|
// Prevent writing to stderr during this render.
|
||||||
const err = console.error;
|
const err = console.error;
|
||||||
|
|||||||
@ -1,31 +1,61 @@
|
|||||||
import { splitArray } from "../splitArray";
|
import { bifurcateArray, splitArray } from "../splitArray";
|
||||||
|
|
||||||
describe("split array on element tests", () => {
|
describe("split array on element tests", () => {
|
||||||
test("empty array", () => {
|
it("empty array", () => {
|
||||||
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
|
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("one element, not in array", () => {
|
it("one element, not in array", () => {
|
||||||
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
|
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ten elements, not in array", () => {
|
it("ten elements, not in array", () => {
|
||||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("one elements, in array", () => {
|
it("one elements, in array", () => {
|
||||||
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
|
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ten elements, in front array", () => {
|
it("ten elements, in front array", () => {
|
||||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ten elements, in middle array", () => {
|
it("ten elements, in middle array", () => {
|
||||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ten elements, in end array", () => {
|
it("ten elements, in end array", () => {
|
||||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("bifurcateArray", () => {
|
||||||
|
it("should return tuple of empty arrays from empty array", () => {
|
||||||
|
const [left, right] = bifurcateArray([], () => true);
|
||||||
|
|
||||||
|
expect(left).toStrictEqual([]);
|
||||||
|
expect(right).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all true condition returning items in the right array", () => {
|
||||||
|
const [left, right] = bifurcateArray([1, 2, 3], () => true);
|
||||||
|
|
||||||
|
expect(left).toStrictEqual([]);
|
||||||
|
expect(right).toStrictEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all false condition returning items in the right array", () => {
|
||||||
|
const [left, right] = bifurcateArray([1, 2, 3], () => false);
|
||||||
|
|
||||||
|
expect(left).toStrictEqual([1, 2, 3]);
|
||||||
|
expect(right).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should split array as specified", () => {
|
||||||
|
const [left, right] = bifurcateArray([1, 2, 3], (i) => Boolean(i % 2));
|
||||||
|
|
||||||
|
expect(left).toStrictEqual([2]);
|
||||||
|
expect(right).toStrictEqual([1, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// Moved from dashboard/client/utils/arrays.ts
|
|
||||||
/**
|
/**
|
||||||
* This function splits an array into two sub arrays on the first instance of
|
* This function splits an array into two sub arrays on the first instance of
|
||||||
* element (from the left). If the array does not contain the element. The
|
* element (from the left). If the array does not contain the element. The
|
||||||
@ -19,3 +18,20 @@ export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
|||||||
|
|
||||||
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits an array into two parts based on the outcome of `condition`. If `true`
|
||||||
|
* the value will be returned as part of the right array. If `false` then part of
|
||||||
|
* the left array.
|
||||||
|
* @param src the full array to bifurcate
|
||||||
|
* @param condition the function to determine which set each is in
|
||||||
|
*/
|
||||||
|
export function bifurcateArray<T>(src: T[], condition: (item: T) => boolean): [falses: T[], trues: T[]] {
|
||||||
|
const res: [T[], T[]] = [[], []];
|
||||||
|
|
||||||
|
for (const item of src) {
|
||||||
|
res[+condition(item)].push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale
|
|||||||
import { CommandContainer } from "./command-palette/command-container";
|
import { CommandContainer } from "./command-palette/command-container";
|
||||||
import { KubeObjectStore } from "../kube-object.store";
|
import { KubeObjectStore } from "../kube-object.store";
|
||||||
import { clusterContext } from "./context";
|
import { clusterContext } from "./context";
|
||||||
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -84,7 +85,7 @@ export class App extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], {
|
||||||
preload: true,
|
preload: true,
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
|||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { IClusterViewRouteParams } from "./cluster-view.route";
|
import { IClusterViewRouteParams } from "./cluster-view.route";
|
||||||
import { ClusterStatus } from "./cluster-status";
|
import { ClusterStatus } from "./cluster-status";
|
||||||
import { hasLoadedView, initView, refreshViews } from "./lens-views";
|
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
import { requestMain } from "../../../common/ipc";
|
import { requestMain } from "../../../common/ipc";
|
||||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { catalogURL } from "../+catalog";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -29,8 +31,14 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, (clusterId) => {
|
reaction(() => this.clusterId, (clusterId) => {
|
||||||
this.showCluster(clusterId);
|
this.showCluster(clusterId);
|
||||||
}, {
|
}, { fireImmediately: true}
|
||||||
fireImmediately: true,
|
),
|
||||||
|
reaction(() => this.cluster?.ready, (ready) => {
|
||||||
|
const clusterView = lensViews.get(this.clusterId);
|
||||||
|
|
||||||
|
if (clusterView && clusterView.isLoaded && !ready) {
|
||||||
|
navigate(catalogURL());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
import { catalogURL } from "../+catalog";
|
|
||||||
|
|
||||||
export interface LensView {
|
export interface LensView {
|
||||||
isLoaded?: boolean
|
isLoaded?: boolean
|
||||||
@ -55,8 +53,6 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
|||||||
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
||||||
lensViews.delete(clusterId);
|
lensViews.delete(clusterId);
|
||||||
|
|
||||||
const wasVisible = iframe.style.display !== "none";
|
|
||||||
|
|
||||||
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
|
// Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed.
|
||||||
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
|
// In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx)
|
||||||
// Issue: https://github.com/lensapp/lens/issues/811
|
// Issue: https://github.com/lensapp/lens/issues/811
|
||||||
@ -64,10 +60,6 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
|||||||
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
|
iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`;
|
||||||
iframe.removeAttribute("name");
|
iframe.removeAttribute("name");
|
||||||
iframe.contentWindow.postMessage("teardown", "*");
|
iframe.contentWindow.postMessage("teardown", "*");
|
||||||
|
|
||||||
if (wasVisible) {
|
|
||||||
navigate(catalogURL());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshViews(visibleClusterId?: string) {
|
export function refreshViews(visibleClusterId?: string) {
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
text-shadow: 0 0 4px #0000008f;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
div.MuiAvatar-colorDefault {
|
div.MuiAvatar-colorDefault {
|
||||||
font-weight:500;
|
font-weight:500;
|
||||||
@ -25,7 +27,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
|
&:not(.active) {
|
||||||
|
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isDragging {
|
&.isDragging {
|
||||||
|
|||||||
@ -46,12 +46,32 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.isDraggingOver {
|
&.isDraggingOver {
|
||||||
box-shadow: 0 0 0px 3px $clusterMenuBackground, 0 0 0px 6px #fff;
|
background-color: #3e4148;
|
||||||
|
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
|
||||||
|
|
||||||
|
&:not(.isDraggingOwner) {
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
> div:not(:empty) {
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 9px #00000042;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animateUp {
|
||||||
|
> div {
|
||||||
|
transform: translate(0px, -40px)!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animateDown {
|
||||||
|
> div {
|
||||||
|
transform: translate(0px, 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animating {
|
&.animating {
|
||||||
transform: translateZ(0); // Remove flickering artifacts
|
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
|
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
@ -60,7 +80,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:not(:empty) {
|
&:not(:empty) {
|
||||||
animation: outline 1s cubic-bezier(0.19, 1, 0.22, 1);
|
.HotbarIcon {
|
||||||
|
animation: click .1s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,13 +107,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use theme-aware colors
|
@keyframes click {
|
||||||
@keyframes outline {
|
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0px 11px $clusterMenuBackground, 0 0 0px 15px #ffffff00;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0px 3px $clusterMenuBackground, 0 0 0px 6px #ffffff;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,6 +52,12 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
hotbar.removeFromHotbar(uid);
|
hotbar.removeFromHotbar(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMoveAwayDirection(entityId: string, cellIndex: number) {
|
||||||
|
const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId);
|
||||||
|
|
||||||
|
return draggableItemIndex > cellIndex ? "animateDown" : "animateUp";
|
||||||
|
}
|
||||||
|
|
||||||
renderGrid() {
|
renderGrid() {
|
||||||
return this.hotbar.items.map((item, index) => {
|
return this.hotbar.items.map((item, index) => {
|
||||||
const entity = this.getEntity(item);
|
const entity = this.getEntity(item);
|
||||||
@ -63,7 +69,10 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
index={index}
|
index={index}
|
||||||
key={entity ? entity.getId() : `cell${index}`}
|
key={entity ? entity.getId() : `cell${index}`}
|
||||||
innerRef={provided.innerRef}
|
innerRef={provided.innerRef}
|
||||||
className={cssNames({ isDraggingOver: snapshot.isDraggingOver })}
|
className={cssNames({
|
||||||
|
isDraggingOver: snapshot.isDraggingOver,
|
||||||
|
isDraggingOwner: snapshot.draggingOverWith == entity?.getId(),
|
||||||
|
}, this.getMoveAwayDirection(snapshot.draggingOverWith, index))}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{item && (
|
{item && (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ClusterContext } from "./components/context";
|
import type { ClusterContext } from "./components/context";
|
||||||
|
|
||||||
import { action, computed, observable, reaction, when } from "mobx";
|
import { action, computed, observable, reaction, when } from "mobx";
|
||||||
import { autobind, noop, rejectPromiseBy } from "./utils";
|
import { autobind, bifurcateArray, noop, rejectPromiseBy } from "./utils";
|
||||||
import { KubeObject, KubeStatus } from "./api/kube-object";
|
import { KubeObject, KubeStatus } from "./api/kube-object";
|
||||||
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
||||||
import { ItemStore } from "./item.store";
|
import { ItemStore } from "./item.store";
|
||||||
@ -281,21 +281,36 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
subscribe(apis = this.getSubscribeApis()) {
|
subscribe(apis = this.getSubscribeApis()) {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
const [clusterScopedApis, namespaceScopedApis] = bifurcateArray(apis, api => api.isNamespaced);
|
||||||
|
|
||||||
// This waits for the context and namespaces to be ready or fails fast if the disposer is called
|
for (const api of namespaceScopedApis) {
|
||||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
const store = apiManager.getStore(api);
|
||||||
.then(() => {
|
|
||||||
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
// This waits for the context and namespaces to be ready or fails fast if the disposer is called
|
||||||
apis.forEach(api => this.watchNamespace(api, "", abortController));
|
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([store.contextReady, store.namespacesReady])])
|
||||||
} else {
|
.then(() => {
|
||||||
apis.forEach(api => {
|
if (
|
||||||
this.loadedNamespaces.forEach((namespace) => {
|
store.context.cluster.isGlobalWatchEnabled
|
||||||
this.watchNamespace(api, namespace, abortController);
|
&& store.loadedNamespaces.length === 0
|
||||||
});
|
) {
|
||||||
});
|
return store.watchNamespace(api, "", abortController);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(noop); // ignore DOMExceptions
|
for (const namespace of this.loadedNamespaces) {
|
||||||
|
store.watchNamespace(api, namespace, abortController);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(noop); // ignore DOMExceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const api of clusterScopedApis) {
|
||||||
|
/**
|
||||||
|
* if the api is cluster scoped then we will never assign to `loadedNamespaces`
|
||||||
|
* and thus `store.namespacesReady` will never resolve. Futhermore, we don't care
|
||||||
|
* about watching namespaces.
|
||||||
|
*/
|
||||||
|
apiManager.getStore(api).watchNamespace(api, "", abortController);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user