mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add icon column to catalog list (#2852)
* add icon column to catalog list Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * Create generic Avatar component Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
parent
5372e3617e
commit
e033122baf
@ -69,7 +69,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableCell.icon {
|
||||||
|
max-width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TableCell.source {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
.TableCell.status {
|
.TableCell.status {
|
||||||
|
max-width: 100px;
|
||||||
&.connected {
|
&.connected {
|
||||||
color: var(--colorSuccess);
|
color: var(--colorSuccess);
|
||||||
}
|
}
|
||||||
@ -97,4 +108,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalogIcon {
|
||||||
|
font-size: 10px;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { CatalogAddButton } from "./catalog-add-button";
|
|||||||
import type { RouteComponentProps } from "react-router";
|
import type { RouteComponentProps } from "react-router";
|
||||||
import type { ICatalogViewRouteParam } from "./catalog.route";
|
import type { ICatalogViewRouteParam } from "./catalog.route";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -174,6 +175,24 @@ export class Catalog extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderIcon(item: CatalogEntityItem) {
|
||||||
|
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
title={item.name}
|
||||||
|
colorHash={`${item.name}-${item.source}`}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="catalogIcon"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.catalogEntityStore) {
|
if (!this.catalogEntityStore) {
|
||||||
return null;
|
return null;
|
||||||
@ -202,12 +221,14 @@ export class Catalog extends React.Component<Props> {
|
|||||||
(entity: CatalogEntityItem) => entity.searchFields,
|
(entity: CatalogEntityItem) => entity.searchFields,
|
||||||
]}
|
]}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
|
{ title: "", className: "icon" },
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
{ title: "Source", className: "source", sortBy: sortBy.source },
|
{ title: "Source", className: "source", sortBy: sortBy.source },
|
||||||
{ title: "Labels", className: "labels" },
|
{ title: "Labels", className: "labels" },
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(item: CatalogEntityItem) => [
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
this.renderIcon(item),
|
||||||
item.name,
|
item.name,
|
||||||
item.source,
|
item.source,
|
||||||
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
|||||||
92
src/renderer/components/avatar/avatar.tsx
Normal file
92
src/renderer/components/avatar/avatar.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { DOMAttributes } from "react";
|
||||||
|
import randomColor from "randomcolor";
|
||||||
|
import GraphemeSplitter from "grapheme-splitter";
|
||||||
|
import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core";
|
||||||
|
import { iter } from "../../utils";
|
||||||
|
|
||||||
|
interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
|
||||||
|
title: string;
|
||||||
|
colorHash?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
src?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameParts(name: string): string[] {
|
||||||
|
const byWhitespace = name.split(/\s+/);
|
||||||
|
|
||||||
|
if (byWhitespace.length > 1) {
|
||||||
|
return byWhitespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byDashes = name.split(/[-_]+/);
|
||||||
|
|
||||||
|
if (byDashes.length > 1) {
|
||||||
|
return byDashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.split(/@+/);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconString(title: string) {
|
||||||
|
if (!title) {
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rawFirst, rawSecond, rawThird] = getNameParts(title);
|
||||||
|
const splitter = new GraphemeSplitter();
|
||||||
|
const first = splitter.iterateGraphemes(rawFirst);
|
||||||
|
const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first;
|
||||||
|
const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty();
|
||||||
|
|
||||||
|
return [
|
||||||
|
...iter.take(first, 1),
|
||||||
|
...iter.take(second, 1),
|
||||||
|
...iter.take(third, 1),
|
||||||
|
].filter(Boolean).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Avatar(props: Props) {
|
||||||
|
const { title, src, width = 32, height = 32, colorHash, ...settings } = props;
|
||||||
|
|
||||||
|
const generateAvatarStyle = (): React.CSSProperties => {
|
||||||
|
return {
|
||||||
|
backgroundColor: randomColor({ seed: colorHash, luminosity: "dark" }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
textTransform: "uppercase"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaterialAvatar
|
||||||
|
variant="rounded"
|
||||||
|
style={generateAvatarStyle()}
|
||||||
|
{...settings}
|
||||||
|
>
|
||||||
|
{getIconString(title)}
|
||||||
|
</MaterialAvatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,9 +20,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.HotbarMenu {
|
.HotbarMenu {
|
||||||
.HotbarIcon {
|
|
||||||
--size: 37px;
|
|
||||||
|
|
||||||
|
.HotbarIconMenu {
|
||||||
|
left: 30px;
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
li.MenuItem {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.HotbarIcon {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -53,9 +62,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.MuiAvatar-root {
|
div.MuiAvatar-root {
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
||||||
transition: all 0s 0.8s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -105,14 +116,4 @@
|
|||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.HotbarIconMenu {
|
|
||||||
left: 30px;
|
|
||||||
min-width: 250px;
|
|
||||||
|
|
||||||
li.MenuItem {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,16 +22,14 @@
|
|||||||
import "./hotbar-icon.scss";
|
import "./hotbar-icon.scss";
|
||||||
|
|
||||||
import React, { DOMAttributes, useState } from "react";
|
import React, { DOMAttributes, useState } from "react";
|
||||||
import { Avatar } from "@material-ui/core";
|
|
||||||
import randomColor from "randomcolor";
|
|
||||||
import GraphemeSplitter from "grapheme-splitter";
|
|
||||||
|
|
||||||
import type { CatalogEntityContextMenu } from "../../../common/catalog";
|
import type { CatalogEntityContextMenu } from "../../../common/catalog";
|
||||||
import { cssNames, IClassName, iter } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { Menu, MenuItem } from "../menu";
|
import { Menu, MenuItem } from "../menu";
|
||||||
import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip";
|
import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
uid: string;
|
uid: string;
|
||||||
@ -44,12 +42,6 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateAvatarStyle(seed: string): React.CSSProperties {
|
|
||||||
return {
|
|
||||||
"backgroundColor": randomColor({ seed, luminosity: "dark" })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
if (menuItem.confirm) {
|
if (menuItem.confirm) {
|
||||||
ConfirmDialog.open({
|
ConfirmDialog.open({
|
||||||
@ -67,22 +59,6 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNameParts(name: string): string[] {
|
|
||||||
const byWhitespace = name.split(/\s+/);
|
|
||||||
|
|
||||||
if (byWhitespace.length > 1) {
|
|
||||||
return byWhitespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const byDashes = name.split(/[-_]+/);
|
|
||||||
|
|
||||||
if (byDashes.length > 1) {
|
|
||||||
return byDashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.split(/@+/);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
||||||
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
||||||
const id = `hotbarIcon-${uid}`;
|
const id = `hotbarIcon-${uid}`;
|
||||||
@ -92,36 +68,18 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
|||||||
setMenuOpen(!menuOpen);
|
setMenuOpen(!menuOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIconString = () => {
|
|
||||||
if (!title) {
|
|
||||||
return "??";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [rawFirst, rawSecond, rawThird] = getNameParts(title);
|
|
||||||
const splitter = new GraphemeSplitter();
|
|
||||||
const first = splitter.iterateGraphemes(rawFirst);
|
|
||||||
const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first;
|
|
||||||
const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty();
|
|
||||||
|
|
||||||
return [
|
|
||||||
...iter.take(first, 1),
|
|
||||||
...iter.take(second, 1),
|
|
||||||
...iter.take(third, 1),
|
|
||||||
].filter(Boolean).join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
|
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
|
||||||
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
|
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
|
||||||
<div id={id}>
|
<div id={id}>
|
||||||
<Avatar
|
<Avatar
|
||||||
{...rest}
|
{...rest}
|
||||||
variant="square"
|
title={title}
|
||||||
|
colorHash={`${title}-${source}`}
|
||||||
className={active ? "active" : "default"}
|
className={active ? "active" : "default"}
|
||||||
style={generateAvatarStyle(`${title}-${source}`)}
|
width={40}
|
||||||
>
|
height={40}
|
||||||
{getIconString()}
|
/>
|
||||||
</Avatar>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</MaterialTooltip>
|
</MaterialTooltip>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user