Remove unused labels components (#12513)
parent
96ed02fdae
commit
3e9234f999
|
@ -6,9 +6,6 @@ import classnames from 'classnames'
|
|||
// Types
|
||||
import {ComponentSize, Greys} from 'src/clockface/types'
|
||||
|
||||
// Components
|
||||
import LabelContainer from 'src/clockface/components/label/LabelContainer'
|
||||
|
||||
// Styles
|
||||
import './Label.scss'
|
||||
|
||||
|
@ -41,8 +38,6 @@ class Label extends Component<Props, State> {
|
|||
testID: 'label--pill',
|
||||
}
|
||||
|
||||
public static Container = LabelContainer
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
Label Container Styles
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import 'src/style/modules';
|
||||
|
||||
$label-margin: 1px;
|
||||
$label-edit-button-diameter: 18px;
|
||||
|
||||
.label--container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label--container-margin {
|
||||
width: calc(100% + #{$label-margin * 2});
|
||||
position: relative;
|
||||
left: $label-margin * -2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $label-margin;
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Additional Labels Indicator
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.additional-labels {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
font-weight: 700;
|
||||
margin: $label-margin;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
background-color: rgba($g6-smoke, 0.5);
|
||||
color: $g11-sidewalk;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
}
|
||||
|
||||
.label--tooltip {
|
||||
z-index: 9999;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition-property: all;
|
||||
}
|
||||
|
||||
.label--tooltip-container {
|
||||
@extend %drop-shadow;
|
||||
opacity: 0;
|
||||
background-color: $g0-obsidian;
|
||||
border-radius: $radius;
|
||||
padding: $ix-marg-c;
|
||||
margin-top: $ix-marg-b + $ix-border;
|
||||
transition: opacity 0.25s ease;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border: $ix-marg-b solid transparent;
|
||||
border-bottom-color: $g0-obsidian;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip-container {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
Edit Labels on Resource
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.label--edit-button {
|
||||
width: $label-edit-button-diameter;
|
||||
height: $label-edit-button-diameter;
|
||||
margin: $label-margin;
|
||||
position: relative;
|
||||
transition: opacity 0.25s ease;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
background-color: $g20-white;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
z-index: 3;
|
||||
transition: width 0.25s ease;
|
||||
height: $ix-border;
|
||||
width: $label-edit-button-diameter - $ix-marg-b;
|
||||
border-radius: $ix-border / 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: translate(-50%,-50%) rotate(90deg);
|
||||
}
|
||||
|
||||
> .button.button-sm {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: $label-edit-button-diameter;
|
||||
height: $label-edit-button-diameter;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
color: transparent;
|
||||
transition:
|
||||
background-color 0.25s ease,
|
||||
border-color 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
height 0.25s ease,
|
||||
width 0.25s ease;
|
||||
|
||||
> .button-icon {
|
||||
font-size: $form-sm-font;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .button.button-sm {
|
||||
width: $form-sm-height - $ix-marg-a;
|
||||
height: $form-sm-height - $ix-marg-a;
|
||||
}
|
||||
|
||||
&:hover:before,
|
||||
&:hover:after {
|
||||
width: $form-sm-height - $ix-marg-c;
|
||||
}
|
||||
}
|
||||
|
||||
/* When used inside an index list, hide until row hover */
|
||||
.index-list--row-cell .label--edit-button {
|
||||
opacity: 0;
|
||||
}
|
||||
.index-list--row-cell:hover .label--edit-button {
|
||||
opacity: 1;
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import LabelTooltip from 'src/clockface/components/label/LabelTooltip'
|
||||
import {Button} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {ButtonShape, IconFont, ComponentColor} from '@influxdata/clockface'
|
||||
|
||||
// Styles
|
||||
import 'src/clockface/components/label/LabelContainer.scss'
|
||||
|
||||
interface Props {
|
||||
children?: JSX.Element[]
|
||||
className?: string
|
||||
limitChildCount?: number
|
||||
resourceName?: string
|
||||
onEdit?: () => void
|
||||
testID?: string
|
||||
}
|
||||
|
||||
class LabelContainer extends Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
limitChildCount: 999,
|
||||
resourceName: 'this resource',
|
||||
testID: 'labels-con',
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {className, testID} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('label--container', {
|
||||
[`${className}`]: className,
|
||||
})}
|
||||
data-testid={testID}
|
||||
>
|
||||
<div className="label--container-margin">
|
||||
{this.children}
|
||||
{this.additionalChildrenIndicator}
|
||||
{this.editButton}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get sortedChildren(): JSX.Element[] {
|
||||
const {children} = this.props
|
||||
|
||||
if (children && React.Children.count(children) > 1) {
|
||||
return children.sort((a: JSX.Element, b: JSX.Element) => {
|
||||
const textA = a.props.name.toUpperCase()
|
||||
const textB = b.props.name.toUpperCase()
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0
|
||||
})
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
private get children(): JSX.Element[] | JSX.Element {
|
||||
const {children, limitChildCount} = this.props
|
||||
|
||||
if (children) {
|
||||
return React.Children.map(
|
||||
this.sortedChildren,
|
||||
(child: JSX.Element, i: number) => {
|
||||
if (i < limitChildCount) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get additionalChildrenIndicator(): JSX.Element {
|
||||
const {children, limitChildCount} = this.props
|
||||
|
||||
const childCount = React.Children.count(children)
|
||||
|
||||
if (limitChildCount < childCount) {
|
||||
const additionalCount = childCount - limitChildCount
|
||||
return (
|
||||
<div className="additional-labels">
|
||||
+{additionalCount} more
|
||||
<LabelTooltip labels={this.sortedChildren.slice(limitChildCount)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get editButton(): JSX.Element {
|
||||
const {onEdit, children, resourceName} = this.props
|
||||
|
||||
const titleText = React.Children.count(children)
|
||||
? `Edit Labels for ${resourceName}`
|
||||
: `Add Labels to ${resourceName}`
|
||||
|
||||
if (onEdit) {
|
||||
return (
|
||||
<div className="label--edit-button">
|
||||
<Button
|
||||
color={ComponentColor.Primary}
|
||||
titleText={titleText}
|
||||
onClick={onEdit}
|
||||
shape={ButtonShape.Square}
|
||||
icon={IconFont.Plus}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
export default LabelContainer
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
Label Selector Styles
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import 'src/style/modules';
|
||||
|
||||
.label-selector--input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label-selector--menu-container {
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
@include gradient-h($g2-kevlar, $g4-onyx);
|
||||
border-radius: $radius;
|
||||
box-shadow: 0 2px 5px 0.6px fade-out($g0-obsidian, 0.7);
|
||||
}
|
||||
|
||||
.label-selector--menu-container .fancy-scroll--track-h {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.label-selector--menu {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: $ix-marg-b - ($ix-border / 2);
|
||||
}
|
||||
|
||||
.label-selector--menu-item {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
margin: $ix-border / 2;
|
||||
}
|
||||
|
||||
.label-selector--empty {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: $g9-mountain;
|
||||
font-style: italic;
|
||||
min-height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: $ix-marg-b - ($ix-border / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.label-selector--selection {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: $ix-marg-c;
|
||||
}
|
||||
|
||||
.label-selector--remove-all {
|
||||
margin-top: $ix-marg-b;
|
||||
margin-left: $ix-marg-b;
|
||||
}
|
||||
|
||||
.label-selector--selected,
|
||||
.label-selector--none-selected {
|
||||
flex: 1 0 0;
|
||||
margin-top: $ix-marg-b;
|
||||
}
|
||||
|
||||
.label-selector--none-selected {
|
||||
font-size: 13px;
|
||||
color: $g10-wolf;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
user-select: none;
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
// Libraries
|
||||
import React, {Component, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// APIs
|
||||
import {client} from 'src/utils/api'
|
||||
|
||||
// Components
|
||||
import {Button} from '@influxdata/clockface'
|
||||
import Input from 'src/clockface/components/inputs/Input'
|
||||
import Label from 'src/clockface/components/label/Label'
|
||||
import LabelContainer from 'src/clockface/components/label/LabelContainer'
|
||||
import LabelSelectorMenu from 'src/clockface/components/label/LabelSelectorMenu'
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
|
||||
// Types
|
||||
import {ComponentSize} from 'src/clockface/types'
|
||||
import {Label as APILabel} from 'src/types/v2/labels'
|
||||
|
||||
// Styles
|
||||
import './LabelSelector.scss'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
enum ArrowDirection {
|
||||
Up = -1,
|
||||
Down = 1,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedLabels: APILabel[]
|
||||
allLabels: APILabel[]
|
||||
onAddLabel: (label: APILabel) => void
|
||||
onRemoveLabel: (label: APILabel) => void
|
||||
onRemoveAllLabels: () => void
|
||||
onCreateLabel: (label: APILabel) => void
|
||||
resourceType: string
|
||||
inputSize?: ComponentSize
|
||||
}
|
||||
|
||||
interface State {
|
||||
filterValue: string
|
||||
filteredLabels: APILabel[]
|
||||
isSuggesting: boolean
|
||||
highlightedID: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class LabelSelector extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
inputSize: ComponentSize.Small,
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
const initialFilteredLabels = _.differenceBy(
|
||||
props.allLabels,
|
||||
props.selectedLabels,
|
||||
label => label.name
|
||||
)
|
||||
|
||||
this.state = {
|
||||
highlightedID: null,
|
||||
filterValue: '',
|
||||
filteredLabels: initialFilteredLabels,
|
||||
isSuggesting: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.handleStartSuggesting()
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="label-selector">
|
||||
<div className="label-selector--selection">
|
||||
{this.selectedLabels}
|
||||
{this.clearSelectedButton}
|
||||
</div>
|
||||
<ClickOutside onClickOutside={this.handleStopSuggesting}>
|
||||
{this.input}
|
||||
</ClickOutside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get selectedLabels(): JSX.Element {
|
||||
const {selectedLabels, resourceType} = this.props
|
||||
|
||||
if (selectedLabels && selectedLabels.length) {
|
||||
return (
|
||||
<LabelContainer className="label-selector--selected">
|
||||
{selectedLabels.map(label => (
|
||||
<Label
|
||||
key={label.name}
|
||||
name={label.name}
|
||||
id={label.name}
|
||||
colorHex={label.properties.color}
|
||||
onDelete={this.handleDelete}
|
||||
description={label.properties.description}
|
||||
/>
|
||||
))}
|
||||
</LabelContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="label-selector--none-selected">{`${_.upperFirst(
|
||||
resourceType
|
||||
)} has no labels`}</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get suggestionMenu(): JSX.Element {
|
||||
const {allLabels, selectedLabels} = this.props
|
||||
const {isSuggesting, highlightedID, filterValue} = this.state
|
||||
|
||||
const allLabelsUsed = allLabels.length === selectedLabels.length
|
||||
|
||||
if (isSuggesting) {
|
||||
return (
|
||||
<LabelSelectorMenu
|
||||
filterValue={filterValue}
|
||||
allLabelsUsed={allLabelsUsed}
|
||||
filteredLabels={this.availableLabels}
|
||||
highlightItemID={highlightedID}
|
||||
onItemClick={this.handleAddLabel}
|
||||
onItemHighlight={this.handleItemHighlight}
|
||||
onCreateLabel={this.handleCreateLabel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get input(): JSX.Element {
|
||||
const {resourceType, inputSize} = this.props
|
||||
const {filterValue} = this.state
|
||||
|
||||
return (
|
||||
<div className="label-selector--input">
|
||||
<Input
|
||||
placeholder={`Add labels to ${resourceType}`}
|
||||
value={filterValue}
|
||||
onFocus={this.handleStartSuggesting}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
size={inputSize}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{this.suggestionMenu}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleAddLabel = (labelID: string): void => {
|
||||
const {onAddLabel, allLabels} = this.props
|
||||
|
||||
const label = allLabels.find(label => label.name === labelID)
|
||||
|
||||
onAddLabel(label)
|
||||
this.handleStopSuggesting()
|
||||
}
|
||||
|
||||
private handleItemHighlight = (highlightedID: string): void => {
|
||||
this.setState({highlightedID})
|
||||
}
|
||||
|
||||
private handleStartSuggesting = () => {
|
||||
const {availableLabels} = this
|
||||
const {isSuggesting} = this.state
|
||||
|
||||
if (_.isEmpty(availableLabels) && !isSuggesting) {
|
||||
return this.setState({
|
||||
isSuggesting: true,
|
||||
highlightedID: null,
|
||||
filterValue: '',
|
||||
})
|
||||
}
|
||||
|
||||
const highlightedID = this.availableLabels[0].name
|
||||
this.setState({isSuggesting: true, highlightedID, filterValue: ''})
|
||||
}
|
||||
|
||||
private handleStopSuggesting = () => {
|
||||
const {allLabels: filteredLabels} = this.props
|
||||
|
||||
this.setState({isSuggesting: false, filterValue: '', filteredLabels})
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
let highlightedID = this.state.highlightedID
|
||||
const {allLabels, selectedLabels} = this.props
|
||||
const filterValue = e.target.value
|
||||
|
||||
const availableLabels = _.differenceBy(
|
||||
allLabels,
|
||||
selectedLabels,
|
||||
l => l.name
|
||||
)
|
||||
|
||||
const filteredLabels = availableLabels.filter(label => {
|
||||
return label.name.includes(filterValue)
|
||||
})
|
||||
|
||||
const highlightedIDAvailable = filteredLabels.find(
|
||||
al => al.name === highlightedID
|
||||
)
|
||||
|
||||
if (!highlightedIDAvailable && filteredLabels.length) {
|
||||
highlightedID = filteredLabels[0].name
|
||||
}
|
||||
|
||||
if (filterValue.length === 0) {
|
||||
return this.setState({
|
||||
isSuggesting: true,
|
||||
filteredLabels: this.props.allLabels,
|
||||
highlightedID: null,
|
||||
filterValue: '',
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({filterValue, filteredLabels, highlightedID})
|
||||
}
|
||||
|
||||
private get availableLabels(): APILabel[] {
|
||||
const {selectedLabels} = this.props
|
||||
const {filteredLabels} = this.state
|
||||
|
||||
return _.differenceBy(filteredLabels, selectedLabels, label => label.name)
|
||||
}
|
||||
|
||||
private handleDelete = (labelID: string): void => {
|
||||
const {onRemoveLabel, selectedLabels} = this.props
|
||||
|
||||
const label = selectedLabels.find(l => l.name === labelID)
|
||||
|
||||
onRemoveLabel(label)
|
||||
}
|
||||
|
||||
private handleHighlightAdjacentItem = (direction: ArrowDirection): void => {
|
||||
const {highlightedID} = this.state
|
||||
const {availableLabels} = this
|
||||
|
||||
if (!availableLabels.length || !highlightedID) {
|
||||
return null
|
||||
}
|
||||
|
||||
const highlightedIndex = _.findIndex(
|
||||
availableLabels,
|
||||
label => label.name === highlightedID
|
||||
)
|
||||
|
||||
const adjacentIndex = Math.min(
|
||||
Math.max(highlightedIndex + direction, 0),
|
||||
availableLabels.length - 1
|
||||
)
|
||||
|
||||
const adjacentID = availableLabels[adjacentIndex].name
|
||||
|
||||
this.setState({highlightedID: adjacentID})
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
const {highlightedID} = this.state
|
||||
|
||||
if (!highlightedID) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.currentTarget.blur()
|
||||
return this.handleStopSuggesting()
|
||||
case 'Enter':
|
||||
e.currentTarget.blur()
|
||||
return this.handleAddLabel(highlightedID)
|
||||
case 'ArrowUp':
|
||||
return this.handleHighlightAdjacentItem(ArrowDirection.Up)
|
||||
case 'ArrowDown':
|
||||
return this.handleHighlightAdjacentItem(ArrowDirection.Down)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private get clearSelectedButton(): JSX.Element {
|
||||
const {selectedLabels, onRemoveAllLabels} = this.props
|
||||
|
||||
if (_.isEmpty(selectedLabels)) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
text="Remove All Labels"
|
||||
size={ComponentSize.ExtraSmall}
|
||||
customClass="label-selector--remove-all"
|
||||
onClick={onRemoveAllLabels}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleCreateLabel = async (label: APILabel) => {
|
||||
const newLabel = await client.labels.create(label.name, label.properties)
|
||||
this.props.onAddLabel(newLabel)
|
||||
this.handleStopSuggesting()
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelSelector
|
|
@ -1,95 +0,0 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
import LabelSelectorMenuItem from 'src/clockface/components/label/LabelSelectorMenuItem'
|
||||
import ResourceLabelForm from 'src/shared/components/ResourceLabelForm'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Label} from 'src/types/v2/labels'
|
||||
|
||||
interface Props {
|
||||
filterValue: string
|
||||
highlightItemID: string
|
||||
filteredLabels: Label[]
|
||||
onItemClick: (labelID: string) => void
|
||||
onItemHighlight: (labelID: string) => void
|
||||
allLabelsUsed: boolean
|
||||
onCreateLabel: (label: Label) => Promise<void>
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class LabelSelectorMenu extends Component<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="label-selector--menu-container">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={250}>
|
||||
<div className="label-selector--menu">
|
||||
{this.menuItems}
|
||||
{this.emptyText}
|
||||
{this.resourceLabelForm}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get menuItems(): JSX.Element[] | JSX.Element {
|
||||
const {
|
||||
filteredLabels,
|
||||
onItemClick,
|
||||
onItemHighlight,
|
||||
highlightItemID,
|
||||
} = this.props
|
||||
|
||||
if (filteredLabels.length) {
|
||||
return filteredLabels.map(label => (
|
||||
<LabelSelectorMenuItem
|
||||
highlighted={highlightItemID === label.name}
|
||||
key={label.name}
|
||||
name={label.name}
|
||||
id={label.name}
|
||||
description={label.properties.description}
|
||||
colorHex={label.properties.color}
|
||||
onClick={onItemClick}
|
||||
onHighlight={onItemHighlight}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private get emptyText(): JSX.Element {
|
||||
const {allLabelsUsed, filterValue} = this.props
|
||||
|
||||
if (!filterValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
let text = `No labels match "${filterValue}" want to create a new label?`
|
||||
|
||||
if (allLabelsUsed) {
|
||||
text = 'You have somehow managed to add all the labels, wow!'
|
||||
}
|
||||
|
||||
return <div className="label-selector--empty">{text}</div>
|
||||
}
|
||||
|
||||
private get resourceLabelForm(): JSX.Element {
|
||||
const {filterValue, onCreateLabel, filteredLabels} = this.props
|
||||
|
||||
if (!filterValue || filteredLabels.find(l => l.name === filterValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceLabelForm labelName={filterValue} onSubmit={onCreateLabel} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelSelectorMenu
|
|
@ -1,54 +0,0 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import Label from 'src/clockface/components/label/Label'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
highlighted: boolean
|
||||
id: string
|
||||
name: string
|
||||
colorHex: string
|
||||
description: string
|
||||
onClick: (labelID: string) => void
|
||||
onHighlight: (labelID: string) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class LabelSelectorMenuItem extends Component<Props> {
|
||||
public render() {
|
||||
const {name, colorHex, description, id} = this.props
|
||||
|
||||
return (
|
||||
<span
|
||||
className="label-selector--menu-item"
|
||||
onMouseOver={this.handleMouseOver}
|
||||
>
|
||||
<Label
|
||||
onClick={this.handleClick}
|
||||
name={name}
|
||||
description={description}
|
||||
id={id}
|
||||
colorHex={colorHex}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private handleMouseOver = (): void => {
|
||||
const {onHighlight, id} = this.props
|
||||
|
||||
onHighlight(id)
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
const {onClick, id} = this.props
|
||||
|
||||
onClick(id)
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelSelectorMenuItem
|
|
@ -1,14 +0,0 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
|
||||
interface Props {
|
||||
labels: JSX.Element | JSX.Element[]
|
||||
}
|
||||
|
||||
const LabelTooltip: SFC<Props> = ({labels}) => (
|
||||
<div className="label--tooltip">
|
||||
<div className="label--tooltip-container">{labels}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default LabelTooltip
|
|
@ -29,7 +29,6 @@ import Context from './components/context_menu/Context'
|
|||
import FormElement from 'src/clockface/components/form_layout/FormElement'
|
||||
import DraggableResizer from 'src/clockface/components/draggable_resizer/DraggableResizer'
|
||||
import Label from 'src/clockface/components/label/Label'
|
||||
import LabelSelector from 'src/clockface/components/label/LabelSelector'
|
||||
import GridSizer from 'src/clockface/components/grid_sizer/GridSizer'
|
||||
import ResponsiveGridSizer from 'src/clockface/components/grid_sizer/ResponsiveGridSizer'
|
||||
import Select from 'src/clockface/components/Select'
|
||||
|
@ -86,7 +85,6 @@ export {
|
|||
Input,
|
||||
InputType,
|
||||
Label,
|
||||
LabelSelector,
|
||||
MultiSelectDropdown,
|
||||
MultiInputType,
|
||||
MultipleInput,
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
// Libraries
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
|
||||
// Components
|
||||
import TableRow from 'src/dashboards/components/dashboard_index/TableRow'
|
||||
import {Label} from 'src/clockface'
|
||||
|
||||
// Dummy Data
|
||||
import {dashboardWithLabels, dashboard, orgs} from 'mocks/dummyData'
|
||||
|
||||
const setup = (override = {}) => {
|
||||
const props = {
|
||||
dashboard,
|
||||
orgs,
|
||||
onDeleteDashboard: jest.fn(),
|
||||
onCloneDashboard: jest.fn(),
|
||||
onExportDashboard: jest.fn(),
|
||||
onUpdateDashboard: jest.fn(),
|
||||
onEditLabels: jest.fn(),
|
||||
onFilterChange: jest.fn(),
|
||||
showOwnerColumn: true,
|
||||
...override,
|
||||
}
|
||||
|
||||
return shallow(<TableRow {...props} />)
|
||||
}
|
||||
|
||||
describe('Dashboard index row', () => {
|
||||
it('renders with no labels', () => {
|
||||
const wrapper = setup()
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('if there are labels', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = setup({dashboard: dashboardWithLabels})
|
||||
|
||||
const labelContainer = wrapper.find(Label.Container)
|
||||
const labels = wrapper.find(Label)
|
||||
|
||||
expect(labelContainer.exists()).toBe(true)
|
||||
expect(labels.length).toBe(dashboardWithLabels.labels.length)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,222 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import moment from 'moment'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Stack,
|
||||
Button,
|
||||
IconFont,
|
||||
ComponentSize,
|
||||
ComponentColor,
|
||||
} from '@influxdata/clockface'
|
||||
import {
|
||||
IndexList,
|
||||
ConfirmationButton,
|
||||
Label,
|
||||
ComponentSpacer,
|
||||
} from 'src/clockface'
|
||||
import EditableDescription from 'src/shared/components/editable_description/EditableDescription'
|
||||
import FeatureFlag from 'src/shared/components/FeatureFlag'
|
||||
import EditableName from 'src/shared/components/EditableName'
|
||||
|
||||
// Types
|
||||
import {Dashboard, Organization} from 'src/types/v2'
|
||||
import {Alignment} from 'src/clockface'
|
||||
|
||||
// Constants
|
||||
import {
|
||||
UPDATED_AT_TIME_FORMAT,
|
||||
DEFAULT_DASHBOARD_NAME,
|
||||
} from 'src/dashboards/constants'
|
||||
|
||||
interface Props {
|
||||
dashboard: Dashboard
|
||||
orgs: Organization[]
|
||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
onFilterChange: (searchTerm: string) => void
|
||||
showOwnerColumn: boolean
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {dashboard, onDeleteDashboard} = this.props
|
||||
const {id} = dashboard
|
||||
|
||||
return (
|
||||
<IndexList.Row
|
||||
key={`dashboard-id--${id}`}
|
||||
disabled={false}
|
||||
testID={`dashboard-index--row ${id}`}
|
||||
>
|
||||
<IndexList.Cell testID="dashboard-index--cell">
|
||||
<ComponentSpacer
|
||||
stackChildren={Stack.Rows}
|
||||
align={Alignment.Left}
|
||||
stretchToFitWidth={true}
|
||||
>
|
||||
<ComponentSpacer
|
||||
stackChildren={Stack.Columns}
|
||||
align={Alignment.Left}
|
||||
>
|
||||
{this.resourceNames}
|
||||
{this.labels}
|
||||
</ComponentSpacer>
|
||||
<EditableDescription
|
||||
description={dashboard.description}
|
||||
placeholder={`Describe ${dashboard.name}`}
|
||||
onUpdate={this.handleUpdateDescription}
|
||||
/>
|
||||
</ComponentSpacer>
|
||||
</IndexList.Cell>
|
||||
{this.ownerCell}
|
||||
{this.lastModifiedCell}
|
||||
<IndexList.Cell alignment={Alignment.Right} revealOnHover={true}>
|
||||
<ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}>
|
||||
<FeatureFlag>
|
||||
<Button
|
||||
size={ComponentSize.ExtraSmall}
|
||||
color={ComponentColor.Default}
|
||||
text="Export"
|
||||
icon={IconFont.Export}
|
||||
onClick={this.handleExport}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
<Button
|
||||
size={ComponentSize.ExtraSmall}
|
||||
color={ComponentColor.Secondary}
|
||||
text="Clone"
|
||||
icon={IconFont.Duplicate}
|
||||
onClick={this.handleClone}
|
||||
/>
|
||||
<ConfirmationButton
|
||||
text="Delete"
|
||||
size={ComponentSize.ExtraSmall}
|
||||
onConfirm={onDeleteDashboard}
|
||||
returnValue={dashboard}
|
||||
confirmText="Confirm"
|
||||
/>
|
||||
</ComponentSpacer>
|
||||
</IndexList.Cell>
|
||||
</IndexList.Row>
|
||||
)
|
||||
}
|
||||
|
||||
private get ownerCell(): JSX.Element {
|
||||
const {showOwnerColumn} = this.props
|
||||
|
||||
if (showOwnerColumn) {
|
||||
return <IndexList.Cell>{this.ownerName}</IndexList.Cell>
|
||||
}
|
||||
}
|
||||
|
||||
private get resourceNames(): JSX.Element {
|
||||
const {dashboard} = this.props
|
||||
|
||||
return (
|
||||
<EditableName
|
||||
onUpdate={this.handleUpdateDashboard}
|
||||
name={dashboard.name}
|
||||
hrefValue={`/dashboards/${dashboard.id}`}
|
||||
noNameString={DEFAULT_DASHBOARD_NAME}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleUpdateDashboard = async (name: string) => {
|
||||
await this.props.onUpdateDashboard({...this.props.dashboard, name})
|
||||
}
|
||||
|
||||
private get labels(): JSX.Element {
|
||||
const {dashboard} = this.props
|
||||
|
||||
if (!dashboard.labels.length) {
|
||||
return (
|
||||
<Label.Container
|
||||
limitChildCount={4}
|
||||
onEdit={this.handleEditLabels}
|
||||
resourceName="this Dashboard"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Label.Container
|
||||
limitChildCount={4}
|
||||
className="index-list--labels"
|
||||
onEdit={this.handleEditLabels}
|
||||
resourceName="this Dashboard"
|
||||
>
|
||||
{dashboard.labels.map((label, index) => (
|
||||
<Label
|
||||
key={label.id || `label-${index}`}
|
||||
id={label.id}
|
||||
colorHex={label.properties.color}
|
||||
name={label.name}
|
||||
description={label.properties.description}
|
||||
onClick={this.handleLabelClick}
|
||||
/>
|
||||
))}
|
||||
</Label.Container>
|
||||
)
|
||||
}
|
||||
|
||||
private handleLabelClick = (id: string) => {
|
||||
const label = this.props.dashboard.labels.find(l => l.id === id)
|
||||
|
||||
this.props.onFilterChange(label.name)
|
||||
}
|
||||
|
||||
private get lastModifiedCell(): JSX.Element {
|
||||
const {dashboard} = this.props
|
||||
|
||||
const relativeTimestamp = moment(dashboard.meta.updatedAt).fromNow()
|
||||
const absoluteTimestamp = moment(dashboard.meta.updatedAt).format(
|
||||
UPDATED_AT_TIME_FORMAT
|
||||
)
|
||||
|
||||
return (
|
||||
<IndexList.Cell>
|
||||
<span title={absoluteTimestamp}>{relativeTimestamp}</span>
|
||||
</IndexList.Cell>
|
||||
)
|
||||
}
|
||||
|
||||
private handleEditLabels = () => {
|
||||
const {dashboard, onEditLabels} = this.props
|
||||
onEditLabels(dashboard)
|
||||
}
|
||||
|
||||
private get ownerName(): JSX.Element {
|
||||
const {dashboard, orgs} = this.props
|
||||
const ownerOrg = orgs.find(o => o.id === dashboard.orgID)
|
||||
|
||||
return (
|
||||
<Link to={`/organizations/${dashboard.orgID}/members`}>
|
||||
{ownerOrg.name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
private handleUpdateDescription = (description: string): void => {
|
||||
const {onUpdateDashboard} = this.props
|
||||
const dashboard = {...this.props.dashboard, description}
|
||||
|
||||
onUpdateDashboard(dashboard)
|
||||
}
|
||||
|
||||
private handleClone = () => {
|
||||
const {onCloneDashboard, dashboard} = this.props
|
||||
onCloneDashboard(dashboard)
|
||||
}
|
||||
|
||||
private handleExport = () => {
|
||||
const {onExportDashboard, dashboard} = this.props
|
||||
onExportDashboard(dashboard)
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import TableRow from 'src/dashboards/components/dashboard_index/TableRow'
|
||||
|
||||
// Types
|
||||
import {Dashboard, Organization} from 'src/types/v2'
|
||||
|
||||
interface Props {
|
||||
dashboards: Dashboard[]
|
||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
onFilterChange: (searchTerm: string) => void
|
||||
orgs: Organization[]
|
||||
showOwnerColumn: boolean
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
dashboards,
|
||||
onExportDashboard,
|
||||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onUpdateDashboard,
|
||||
onEditLabels,
|
||||
orgs,
|
||||
showOwnerColumn,
|
||||
onFilterChange,
|
||||
} = this.props
|
||||
|
||||
return dashboards.map(d => (
|
||||
<TableRow
|
||||
key={d.id}
|
||||
dashboard={d}
|
||||
onExportDashboard={onExportDashboard}
|
||||
onCloneDashboard={onCloneDashboard}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
onEditLabels={onEditLabels}
|
||||
orgs={orgs}
|
||||
showOwnerColumn={showOwnerColumn}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import {
|
||||
ComponentSize,
|
||||
ComponentColor,
|
||||
ComponentStatus,
|
||||
Button,
|
||||
} from '@influxdata/clockface'
|
||||
import {
|
||||
OverlayContainer,
|
||||
OverlayHeading,
|
||||
OverlayBody,
|
||||
LabelSelector,
|
||||
Grid,
|
||||
Form,
|
||||
} from 'src/clockface'
|
||||
import FetchLabels from 'src/shared/components/FetchLabels'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Label} from 'src/types/v2/labels'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
// Utils
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
interface Props<T> {
|
||||
resource: T
|
||||
onDismissOverlay: () => void
|
||||
onAddLabels: (resourceID: string, labels: Label[]) => void
|
||||
onRemoveLabels: (resourceID: string, labels: Label[]) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedLabels: Label[]
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class EditLabelsOverlay<T> extends PureComponent<Props<T>, State> {
|
||||
constructor(props: Props<T>) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selectedLabels: _.get(props, 'resource.labels'),
|
||||
loading: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onDismissOverlay, resource} = this.props
|
||||
const {selectedLabels} = this.state
|
||||
|
||||
return (
|
||||
<OverlayContainer maxWidth={720}>
|
||||
<OverlayHeading title="Manage Labels" onDismiss={onDismissOverlay} />
|
||||
<OverlayBody>
|
||||
<Form>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<Form.Element label="">
|
||||
<Form.Box>
|
||||
<FetchLabels>
|
||||
{labels => (
|
||||
<LabelSelector
|
||||
inputSize={ComponentSize.Medium}
|
||||
allLabels={labels}
|
||||
resourceType={_.get(resource, 'name')}
|
||||
selectedLabels={selectedLabels}
|
||||
onAddLabel={this.handleAddLabel}
|
||||
onRemoveLabel={this.handleRemoveLabel}
|
||||
onRemoveAllLabels={this.handleRemoveAllLabels}
|
||||
/>
|
||||
)}
|
||||
</FetchLabels>
|
||||
</Form.Box>
|
||||
</Form.Element>
|
||||
<Form.Footer>
|
||||
<Button text="Cancel" onClick={onDismissOverlay} />
|
||||
<Button
|
||||
color={ComponentColor.Success}
|
||||
text="Save Changes"
|
||||
onClick={this.handleSaveLabels}
|
||||
status={this.buttonStatus}
|
||||
customClass="resource-labels--save-edits"
|
||||
/>
|
||||
</Form.Footer>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Form>
|
||||
</OverlayBody>
|
||||
</OverlayContainer>
|
||||
)
|
||||
}
|
||||
|
||||
private get buttonStatus(): ComponentStatus {
|
||||
if (this.changes.isChanged) {
|
||||
if (this.state.loading === RemoteDataState.Loading) {
|
||||
return ComponentStatus.Loading
|
||||
}
|
||||
|
||||
return ComponentStatus.Default
|
||||
}
|
||||
|
||||
return ComponentStatus.Disabled
|
||||
}
|
||||
|
||||
private get changes(): {
|
||||
isChanged: boolean
|
||||
removedLabels: Label[]
|
||||
addedLabels: Label[]
|
||||
} {
|
||||
const {resource} = this.props
|
||||
const {selectedLabels} = this.state
|
||||
const labels = getDeep<Label[]>(resource, 'labels', [])
|
||||
|
||||
const intersection = _.intersectionBy(labels, selectedLabels, 'name')
|
||||
const removedLabels = _.differenceBy(labels, intersection, 'name')
|
||||
const addedLabels = _.differenceBy(selectedLabels, intersection, 'name')
|
||||
|
||||
return {
|
||||
isChanged: !!removedLabels.length || !!addedLabels.length,
|
||||
addedLabels,
|
||||
removedLabels,
|
||||
}
|
||||
}
|
||||
|
||||
private handleRemoveAllLabels = (): void => {
|
||||
this.setState({selectedLabels: []})
|
||||
}
|
||||
|
||||
private handleRemoveLabel = (label: Label): void => {
|
||||
const selectedLabels = this.state.selectedLabels.filter(
|
||||
l => l.name !== label.name
|
||||
)
|
||||
|
||||
this.setState({selectedLabels})
|
||||
}
|
||||
|
||||
private handleAddLabel = (label: Label): void => {
|
||||
const selectedLabels = [...this.state.selectedLabels, label]
|
||||
|
||||
this.setState({selectedLabels})
|
||||
}
|
||||
|
||||
private handleSaveLabels = async (): Promise<void> => {
|
||||
const {onAddLabels, onRemoveLabels, resource, onDismissOverlay} = this.props
|
||||
|
||||
const {addedLabels, removedLabels} = this.changes
|
||||
const resourceID = _.get(resource, 'id')
|
||||
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
|
||||
if (addedLabels.length) {
|
||||
await onAddLabels(resourceID, addedLabels)
|
||||
}
|
||||
|
||||
if (removedLabels.length) {
|
||||
await onRemoveLabels(resourceID, removedLabels)
|
||||
}
|
||||
|
||||
this.setState({loading: RemoteDataState.Done})
|
||||
|
||||
onDismissOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
export default EditLabelsOverlay
|
Loading…
Reference in New Issue