add drag and drop capabilities for the order of cluster icons on the side bar (#623)

* add drag and drop capabilities for the order of cluster icons on the side bar

* add type annotations for better clarity

* fix clusters staying in previously icon reordered workspaces

* add some tests to cluster-store

Signed-off-by: Sebastian Malton <smalton@mirantis.com>

Co-authored-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian MaltonandSebastian Malton authored and GitHub committed 2020-09-04 13:32:27 -04:00
1 parent 7d2215b5b4
commit 8f94f4a3da
7 files changed
+218 -28

No files matched your search

+24 -4
View File
@@ -1,4 +1,4 @@
import type { WorkspaceId } from "./workspace-store";
import { WorkspaceId, workspaceStore } from "./workspace-store";
import path from "path";
import { app, ipcRenderer, remote } from "electron";
import { unlink } from "fs-extra";
@@ -11,6 +11,9 @@ import { tracker } from "./tracker";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import _ from "lodash";
import move from "array-move";
import { is } from "immer/dist/internal";
export interface ClusterIconUpload {
clusterId: string;
@@ -48,6 +51,7 @@ export interface ClusterPreferences {
prometheusProvider?: {
type: string;
};
iconOrder?: number;
icon?: string;
httpsProxy?: string;
}
@@ -101,6 +105,20 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
this.activeClusterId = id;
}
@action
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
const clusters = this.getByWorkspaceId(workspace);
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
throw new Error(`invalid from<->to arguments`)
}
move.mutate(clusters, from, to);
for (const i in clusters) {
// This resets the iconOrder to the current display order
clusters[i].preferences.iconOrder = +i;
}
}
hasClusters() {
return this.clusters.size > 0;
}
@@ -114,7 +132,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
getByWorkspaceId(workspaceId: string): Cluster[] {
return this.clustersList.filter(cluster => cluster.workspace === workspaceId)
const clusters = Array.from(this.clusters.values())
.filter(cluster => cluster.workspace === workspaceId);
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder)
}
@action
@@ -156,7 +176,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters
clusters.forEach(clusterModel => {
for (const clusterModel of clusters) {
let cluster = currentClusters.get(clusterModel.id);
if (cluster) {
cluster.updateModel(clusterModel);
@@ -164,7 +184,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
cluster = new Cluster(clusterModel);
}
newClusters.set(clusterModel.id, cluster);
});
}
// update removed clusters
currentClusters.forEach(cluster => {
+28
View File
@@ -93,6 +93,34 @@ describe("empty config", () => {
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
})
it("check if reorderring works for same from and to", () => {
clusterStore.swapIconOrders("workstation", 1, 1)
const clusters = clusterStore.getByWorkspaceId("workstation");
expect(clusters[0].id).toBe("prod")
expect(clusters[0].preferences.iconOrder).toBe(0)
expect(clusters[1].id).toBe("dev")
expect(clusters[1].preferences.iconOrder).toBe(1)
});
it("check if reorderring works for different from and to", () => {
clusterStore.swapIconOrders("workstation", 0, 1)
const clusters = clusterStore.getByWorkspaceId("workstation");
expect(clusters[0].id).toBe("dev")
expect(clusters[0].preferences.iconOrder).toBe(0)
expect(clusters[1].id).toBe("prod")
expect(clusters[1].preferences.iconOrder).toBe(1)
});
it("check if after icon reordering, changing workspaces still works", () => {
clusterStore.swapIconOrders("workstation", 1, 1)
clusterStore.getById("prod").workspace = "default"
expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1);
expect(clusterStore.getByWorkspaceId("default").length).toBe(2);
});
it("removes cluster from store", async () => {
await clusterStore.removeById("foo");
expect(clusterStore.getById("foo")).toBeUndefined();
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/clus
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store";
import type { FeatureStatusMap } from "./feature"
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { action, computed, intercept, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler"
@@ -4,6 +4,7 @@
position: relative;
border-radius: $radius;
padding: $radius;
margin-bottom: $padding * 2;
user-select: none;
cursor: pointer;
@@ -10,7 +10,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { cssNames, IClassName } from "../../utils";
import { cssNames, IClassName, autobind } from "../../utils";
import { Badge } from "../badge";
import { navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
@@ -20,6 +20,7 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL, getMatchedClusterId } from "./cluster-view.route";
import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd";
// fixme: allow to rearrange clusters with drag&drop
@@ -87,6 +88,18 @@ export class ClustersMenu extends React.Component<Props> {
})
}
@autobind()
swapClusterIconOrder(result: DropResult) {
if (result.reason === "DROP") {
const { currentWorkspaceId } = workspaceStore;
const {
source: { index: from },
destination: { index: to },
} = result
clusterStore.swapIconOrders(currentWorkspaceId, from, to)
}
}
render() {
const { className } = this.props;
const { newContexts } = userStore;
@@ -94,26 +107,46 @@ export class ClustersMenu extends React.Component<Props> {
return (
<div className={cssNames("ClustersMenu flex column", className)}>
<div className="clusters flex column gaps">
{clusters.map(cluster => {
return (
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={cluster.id === getMatchedClusterId()}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
)
})}
<DragDropContext onDragEnd={this.swapClusterIconOrder}>
<Droppable droppableId="cluster-menu" type="CLUSTER">
{(provided: DroppableProvided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{clusters.map((cluster, index) => (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided: DraggableProvided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ClusterIcon
key={cluster.id}
showErrors={true}
cluster={cluster}
isActive={cluster.id === getMatchedClusterId()}
onClick={() => this.showCluster(cluster.id)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
<div className="add-cluster" onClick={this.addCluster}>
<Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans>
</Tooltip>
<Icon big material="add" id="add-cluster-icon"/>
<Icon big material="add" id="add-cluster-icon" />
{newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
)}
</div>
</div>