From 768e1d14ca8ca335b106545224363c8bf6e097cd Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 6 May 2021 00:37:06 +0300 Subject: [PATCH 1/6] Fixing hotbar drag-n-drop animation issues (#2704) --- src/common/__tests__/hotbar-store.test.ts | 15 ++++++++ .../components/hotbar/hotbar-icon.scss | 6 ++- .../components/hotbar/hotbar-menu.scss | 37 +++++++++++++++---- .../components/hotbar/hotbar-menu.tsx | 16 ++++++-- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 8e4748e515..9a15e9eb67 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -207,6 +207,21 @@ describe("HotbarStore", () => { 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", () => { // Prevent writing to stderr during this render. const err = console.error; diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss index c6134f16e0..275af34d90 100644 --- a/src/renderer/components/hotbar/hotbar-icon.scss +++ b/src/renderer/components/hotbar/hotbar-icon.scss @@ -6,6 +6,8 @@ user-select: none; cursor: pointer; transition: none; + text-shadow: 0 0 4px #0000008f; + position: relative; div.MuiAvatar-colorDefault { font-weight:500; @@ -25,7 +27,9 @@ } &: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 { diff --git a/src/renderer/components/hotbar/hotbar-menu.scss b/src/renderer/components/hotbar/hotbar-menu.scss index 1805fdf4c5..317b8ceefb 100644 --- a/src/renderer/components/hotbar/hotbar-menu.scss +++ b/src/renderer/components/hotbar/hotbar-menu.scss @@ -46,12 +46,32 @@ position: relative; &.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 { - transform: translateZ(0); // Remove flickering artifacts - &:empty { animation: shake .6s cubic-bezier(.36,.07,.19,.97) both; transform: translate3d(0, 0, 0); @@ -60,7 +80,9 @@ } &: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 outline { +@keyframes click { 0% { - box-shadow: 0 0 0px 11px $clusterMenuBackground, 0 0 0px 15px #ffffff00; + margin-top: 0; } 100% { - box-shadow: 0 0 0px 3px $clusterMenuBackground, 0 0 0px 6px #ffffff; + margin-top: 2px; } } \ No newline at end of file diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 29008f073c..a9629b8934 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -48,9 +48,16 @@ export class HotbarMenu extends React.Component { HotbarStore.getInstance().restackItems(from, to); } + getMoveAwayDirection(entityId: string, cellIndex: number) { + const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId); + + return draggableItemIndex > cellIndex ? "animateDown" : "animateUp"; + } + renderGrid() { return this.hotbar.items.map((item, index) => { const entity = this.getEntity(item); + const isActive = !entity ? false : this.isActive(entity); return ( @@ -59,11 +66,14 @@ export class HotbarMenu extends React.Component { index={index} key={entity ? entity.getId() : `cell${index}`} 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} > {entity && ( - + {(provided, snapshot) => { const style = { zIndex: defaultHotbarCells - index, @@ -83,7 +93,7 @@ export class HotbarMenu extends React.Component { key={index} index={index} entity={entity} - isActive={this.isActive(entity)} + isActive={isActive} onClick={() => entity.onRun(catalogEntityRunContext)} className={cssNames({ isDragging: snapshot.isDragging })} /> From 7b1b8e6321a4865ffe9c045d9a59e9ef8443aa8a Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 6 May 2021 02:10:52 -0400 Subject: [PATCH 2/6] Fix ClusterScoped kube APIs not subscribing (#2706) * Fix ClusterScoped kube APIs not subscribing Signed-off-by: Sebastian Malton * remove debug Signed-off-by: Sebastian Malton --- src/common/utils/__tests__/splitArray.test.ts | 46 +++++++++++++++---- src/common/utils/splitArray.ts | 18 +++++++- src/renderer/components/app.tsx | 3 +- src/renderer/kube-object.store.ts | 45 ++++++++++++------ 4 files changed, 87 insertions(+), 25 deletions(-) diff --git a/src/common/utils/__tests__/splitArray.test.ts b/src/common/utils/__tests__/splitArray.test.ts index 1e1589fee2..8228702ea9 100644 --- a/src/common/utils/__tests__/splitArray.test.ts +++ b/src/common/utils/__tests__/splitArray.test.ts @@ -1,31 +1,61 @@ -import { splitArray } from "../splitArray"; +import { bifurcateArray, splitArray } from "../splitArray"; describe("split array on element tests", () => { - test("empty array", () => { + it("empty array", () => { expect(splitArray([], 10)).toStrictEqual([[], [], false]); }); - test("one element, not in array", () => { + it("one element, not in array", () => { 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]); }); - test("one elements, in array", () => { + it("one elements, in array", () => { 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]); }); - 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]); }); - 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]); }); }); + +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]); + }); +}); diff --git a/src/common/utils/splitArray.ts b/src/common/utils/splitArray.ts index 7be367ebe6..7d623a55bc 100644 --- a/src/common/utils/splitArray.ts +++ b/src/common/utils/splitArray.ts @@ -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 * element (from the left). If the array does not contain the element. The @@ -19,3 +18,20 @@ export function splitArray(array: T[], element: T): [T[], T[], boolean] { 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(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; +} diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 41fc63cf4d..23f6f2c508 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -50,6 +50,7 @@ import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale import { CommandContainer } from "./command-palette/command-container"; import { KubeObjectStore } from "../kube-object.store"; import { clusterContext } from "./context"; +import { namespaceStore } from "./+namespaces/namespace.store"; @observer export class App extends React.Component { @@ -84,7 +85,7 @@ export class App extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], { + kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], { preload: true, }) ]); diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 35f052d03f..445caf4cc6 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -1,7 +1,7 @@ import type { ClusterContext } from "./components/context"; 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 { IKubeWatchEvent } from "./api/kube-watch-api"; import { ItemStore } from "./item.store"; @@ -281,21 +281,36 @@ export abstract class KubeObjectStore extends ItemSt subscribe(apis = this.getSubscribeApis()) { 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 - Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) - .then(() => { - if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) { - apis.forEach(api => this.watchNamespace(api, "", abortController)); - } else { - apis.forEach(api => { - this.loadedNamespaces.forEach((namespace) => { - this.watchNamespace(api, namespace, abortController); - }); - }); - } - }) - .catch(noop); // ignore DOMExceptions + for (const api of namespaceScopedApis) { + const store = apiManager.getStore(api); + + // This waits for the context and namespaces to be ready or fails fast if the disposer is called + Promise.race([rejectPromiseBy(abortController.signal), Promise.all([store.contextReady, store.namespacesReady])]) + .then(() => { + if ( + store.context.cluster.isGlobalWatchEnabled + && store.loadedNamespaces.length === 0 + ) { + return store.watchNamespace(api, "", abortController); + } + + 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 () => { abortController.abort(); From a23f3e038ce58810c2a58a5a84b71cc17bfcd4ab Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 6 May 2021 10:26:48 -0400 Subject: [PATCH 3/6] release v5.0.0-beta.3 (#2711) Signed-off-by: Sebastian Malton --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a5fac242d..5fe56235a7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-lens", "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", - "version": "5.0.0-beta.2", + "version": "5.0.0-beta.3", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", From a6e916fa90284c40c0c368ea022f0f546c4fb2ca Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 6 May 2021 20:45:32 +0300 Subject: [PATCH 4/6] Create cluster object also for non-local sources (#2719) Signed-off-by: Jari Kolehmainen --- src/main/cluster-manager.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 3c4bcd0015..9b1b133749 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -62,10 +62,6 @@ export class ClusterManager extends Singleton { @action syncClustersFromCatalog(entities: KubernetesCluster[]) { for (const entity of entities) { - if (entity.metadata.source !== "local") { - continue; - } - const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); if (!cluster) { From 253b3768f67dd347807e137d5d602deaf7ac9923 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 7 May 2021 09:26:26 +0300 Subject: [PATCH 5/6] Fix auto-update exiting too fast (#2721) Signed-off-by: Jari Kolehmainen --- src/main/app-updater.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 77ae2bff18..2f9fc1d317 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -4,7 +4,7 @@ import { isDevelopment, isPublishConfigured, isTestEnv } from "../common/vars"; import { delay } from "../common/utils"; import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; import { once } from "lodash"; -import { app, ipcMain } from "electron"; +import { ipcMain } from "electron"; let installVersion: null | string = null; @@ -17,7 +17,6 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd if (arg.now) { logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); autoUpdater.quitAndInstall(true, true); - app.exit(); // this is needed for the installer not to fail on windows. } else { logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); autoUpdater.autoInstallOnAppQuit = true; From 4d80895dd019a967bca1edba92396b1c215f6e19 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 7 May 2021 10:44:48 +0300 Subject: [PATCH 6/6] v5.0.0-beta.4 (#2722) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fe56235a7..d48c851ba7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-lens", "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", - "version": "5.0.0-beta.3", + "version": "5.0.0-beta.4", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT",