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

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

Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton 2020-07-30 09:48:14 -04:00
parent 435baaea00
commit b0acf0a472
5 changed files with 172 additions and 25 deletions

View File

@ -169,7 +169,9 @@
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.3",
"array-move": "^3.0.0",
"chalk": "^4.1.0",
"conf": "^7.0.1",
"crypto-js": "^4.0.0",
@ -197,6 +199,7 @@
"path-to-regexp": "^6.1.0",
"proper-lockfile": "^4.1.1",
"react-router": "^5.2.0",
"react-beautiful-dnd": "^13.0.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"semver": "^7.3.2",
@ -315,4 +318,4 @@
"xterm": "^4.6.0",
"xterm-addon-fit": "^0.4.0"
}
}
}

View File

@ -18,9 +18,18 @@ export interface ClusterIconUpload {
path: string;
}
interface ClusterIconOrdering {
[workspaceId: string]: ClusterId[]
}
interface ClusterIconOrderingMap {
[id: string]: number
}
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[]
iconOrder?: ClusterIconOrdering,
}
export type ClusterId = string;
@ -83,6 +92,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable activeClusterId: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
@observable clusterOrders: ClusterIconOrdering = {};
@computed get activeCluster(): Cluster | null {
return this.getById(this.activeClusterId);
@ -113,8 +123,26 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.get(id);
}
getByWorkspaceId(workspaceId: string): Cluster[] {
return this.clustersList.filter(cluster => cluster.workspace === workspaceId)
getByWorkspaceId(workspaceId: string, sorted = true): Cluster[] {
const ordering = this.clusterOrders[workspaceId]?.reduce((acc: ClusterIconOrderingMap, cur, i) => { acc[cur] = i; return acc; }, {});
const clusters = this.clustersList.filter(cluster => cluster.workspace === workspaceId);
if (!ordering || !sorted) {
return clusters;
}
const sortedClusters = [];
const unsortedClusters = [];
for (const cluster of clusters) {
const index = ordering[cluster.id];
if (typeof index === "number" && index >= 0) {
sortedClusters[index] = cluster;
} else {
unsortedClusters.push(cluster);
}
}
return [...sortedClusters.filter(c => c), ...unsortedClusters];
}
@action
@ -144,17 +172,19 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
removeByWorkspaceId(workspaceId: string) {
this.getByWorkspaceId(workspaceId).forEach(cluster => {
this.getByWorkspaceId(workspaceId, false).forEach(cluster => {
this.removeById(cluster.id)
})
}
@action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
protected fromStore({ activeCluster, clusters = [], iconOrder = {} }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS();
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
this.clusterOrders = iconOrder;
// update new clusters
clusters.forEach(clusterModel => {
let cluster = currentClusters.get(clusterModel.id);
@ -182,6 +212,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return toJS({
activeCluster: this.activeClusterId,
clusters: this.clustersList.map(cluster => cluster.toJSON()),
iconOrder: this.clusterOrders,
}, {
recurseEverything: true
})

View File

@ -4,6 +4,7 @@
position: relative;
border-radius: $radius;
padding: $radius;
margin-bottom: $padding * 2;
user-select: none;
cursor: pointer;

View File

@ -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,8 @@ 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 } from "react-beautiful-dnd";
import move from "array-move";
// fixme: allow to rearrange clusters with drag&drop
@ -87,33 +89,77 @@ export class ClustersMenu extends React.Component<Props> {
})
}
@autobind()
swapClusterIconOrder(result: DropResult) {
if (result.reason === "DROP") {
const { currentWorkspaceId } = workspaceStore;
const clusters = clusterStore.getByWorkspaceId(currentWorkspaceId);
move.mutate(clusters, result.source.index, result.destination.index);
clusterStore.clusterOrders[currentWorkspaceId] = clusters.map(c => c.id);
}
}
render() {
const { className } = this.props;
const { newContexts } = userStore;
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
return (
<div className={cssNames("ClustersMenu flex column", className)}>
<div
className={cssNames("ClustersMenu flex column", className)}
onMouseEnter={() => this.showHint = false}
>
{showStartupHint && (
<div className="startup-tooltip flex column gaps">
<p><Trans>This is the quick launch menu.</Trans></p>
<p>
<Trans>
Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button.
</Trans>
</p>
</div>
)}
<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) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{clusters.map((cluster, index) => (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided) => (
<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>

View File

@ -1999,7 +1999,14 @@
dependencies:
"@types/react" "*"
"@types/react-dom@*":
"@types/react-beautiful-dnd@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
dependencies:
"@types/react" "*"
"@types/react-dom@*", "@types/react-dom@^16.9.8":
version "16.9.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
@ -2757,6 +2764,11 @@ array-union@^2.1.0:
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array-move@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.0.tgz#b646a2f4980be78f04d28d7572a72036150d364e"
integrity sha512-kqK1ZKiAVfIdfiJjC3zpAGPg3OEkjeeKuOILwS1b+oh34dI6GTg9szgRT+oKWw48RuVF8RGjlWCSYkn6NU+Jvw==
array-unique@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@ -4110,6 +4122,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-element-queries@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/css-element-queries/-/css-element-queries-1.2.3.tgz#e14940b1fcd4bf0da60ea4145d05742d7172e516"
@ -7832,7 +7851,7 @@ memfs@^3.1.2:
dependencies:
fs-monkey "1.0.1"
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0, memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
@ -9455,6 +9474,11 @@ querystring@0.2.0:
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
ramda@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.0.tgz#915dc29865c0800bf3f69b8fd6c279898b59de43"
@ -9493,6 +9517,19 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-beautiful-dnd@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
dependencies:
"@babel/runtime" "^7.8.4"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.1.1"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-dom@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@ -9510,11 +9547,22 @@ react-input-autosize@^2.2.2:
dependencies:
prop-types "^15.5.8"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1:
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-redux@^7.1.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
dependencies:
"@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.9.0"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
@ -9704,6 +9752,14 @@ redent@^1.0.0:
indent-string "^2.1.0"
strip-indent "^1.0.1"
redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@ -10875,6 +10931,11 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -11074,7 +11135,7 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tiny-invariant@^1.0.2:
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@ -11541,6 +11602,11 @@ url@^0.11.0, url@~0.11.0:
punycode "1.3.2"
querystring "0.2.0"
use-memo-one@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c"
integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"