diff --git a/ui/cypress/e2e/dashboardsIndex.test.ts b/ui/cypress/e2e/dashboardsIndex.test.ts index e80b1981d02..f6b1e815752 100644 --- a/ui/cypress/e2e/dashboardsIndex.test.ts +++ b/ui/cypress/e2e/dashboardsIndex.test.ts @@ -1,5 +1,9 @@ import {Organization} from '@influxdata/influx' +const newLabelName = 'click-me' +const dashboardName = 'Bee Happy' +const dashSearchName = 'bEE' + describe('Dashboards', () => { beforeEach(() => { cy.flush() @@ -20,9 +24,7 @@ describe('Dashboards', () => { cy.visit('/dashboards') - cy.getByTestID('resource-card') - .its('length') - .should('be.eq', 1) + cy.getByTestID('dashboard-card').should('have.length', 1) }) it('can create a dashboard from the header', () => { @@ -34,146 +36,162 @@ describe('Dashboards', () => { cy.visit('/dashboards') - cy.getByTestID('resource-card') - .its('length') - .should('be.eq', 1) + cy.getByTestID('dashboard-card').should('have.length', 1) }) - it.skip('can delete a dashboard', () => { - cy.get('@org').then(({id}) => { - cy.createDashboard(id) - cy.createDashboard(id) - }) + describe('Dashboard List', () => { + beforeEach(() => { + cy.get('@org').then(({id}) => { + cy.createDashboard(id, dashboardName).then(({body}) => { + cy.createAndAddLabel('dashboards', body.id, newLabelName) + }) - cy.getByTestID('resource-card') - .its('length') - .should('eq', 2) + cy.createDashboard(id).then(({body}) => { + cy.createAndAddLabel('dashboards', body.id, 'bar') + }) + }) - cy.getByTestID('resource-card') - .first() - .trigger('mouseover') - .within(() => { - cy.getByTestID('context-delete-menu').click() + cy.visit('/dashboards') + }) - cy.getByTestID('context-delete-dashboard').click() - }) + it('can delete a dashboard', () => { + cy.getByTestID('dashboard-card').should('have.length', 2) - cy.getByTestID('resource-card') - .its('length') - .should('eq', 1) - }) + cy.getByTestID('dashboard-card') + .first() + .trigger('mouseover') + .within(() => { + cy.getByTestID('context-delete-menu').click() + cy.getByTestID('context-delete-dashboard').click() + }) - it('can edit a dashboards name', () => { - cy.get('@org').then(({id}) => { - cy.createDashboard(id) + cy.getByTestID('dashboard-card').should('have.length', 1) }) - const newName = 'new 🅱️ashboard' + it('can edit a dashboards name', () => { + const newName = 'new 🅱️ashboard' - cy.getByTestID('resource-card').within(() => { - cy.getByTestID('dashboard-card--name').trigger('mouseover') + cy.getByTestID('dashboard-card').within(() => { + cy.getByTestID('dashboard-card--name') + .first() + .trigger('mouseover') - cy.getByTestID('dashboard-card--name-button').click() + cy.getByTestID('dashboard-card--name-button') + .first() + .click() - cy.get('.input-field') - .type(newName) - .type('{enter}') + cy.get('.input-field') + .type(newName) + .type('{enter}') + }) + + cy.getByTestID('dashboard-card').should('contain', newName) }) - cy.visit('/dashboards') + describe('Labeling', () => { + it('can click to filter dashboard labels', () => { + cy.getByTestID('dashboard-card').should('have.length', 2) - cy.getByTestID('resource-card').should('contain', newName) - }) + cy.getByTestID(`label--pill ${newLabelName}`).click() - describe('labeling', () => { - it('can click to filter dashboard labels', () => { - const newLabelName = 'click-me' - - cy.get('@org').then(({id}) => { - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, newLabelName) - }) - - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, 'bar') - }) + cy.getByTestID('dashboard-card') + .should('have.length', 1) + .and('contain', newLabelName) }) - cy.visit('/dashboards') + it('can delete a label from a dashboard', () => { + cy.getByTestID('dashboard-card') + .first() + .within(() => { + const pillID = `label--pill ${newLabelName}` - cy.getByTestID('resource-card').should('have.length', 2) + cy.getByTestID(pillID).should('have.length', 1) - cy.getByTestID(`label--pill ${newLabelName}`).click() + cy.getByTestID(`label--pill--delete ${newLabelName}`).click({ + force: true, + }) - cy.getByTestID('resource-card').should('have.length', 1) - }) - }) + cy.getByTestID(pillID).should('have.length', 0) + cy.getByTestID(`inline-labels--empty`).should('have.length', 1) + }) + }) - describe('searching', () => { - it('can search dashboards by labels', () => { - const widgetSearch = 'searchME' + it('can add an existing label to a dashboard', () => { + const labelName = 'swogglez' - cy.get('@org').then(({id}) => { - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, widgetSearch) - }) + cy.createLabel(labelName).then(() => { + cy.getByTestID(`inline-labels--add`) + .first() + .click() - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, 'bar') + cy.getByTestID('inline-labels--popover').within(() => { + cy.getByTestID(`label--pill ${labelName}`).click() + }) + + cy.getByTestID('dashboard-card') + .first() + .within(() => { + cy.getByTestID(`label--pill ${labelName}`).should('be.visible') + }) }) }) - cy.visit('/dashboards') - - cy.getByTestID('resource-card').should('have.length', 2) + it('can create a label and add to a dashboard', () => { + const label = 'plerps' + cy.getByTestID(`inline-labels--add`) + .first() + .click() - cy.getByTestID('search-widget').type(widgetSearch) + cy.getByTestID('inline-labels--popover').within(() => { + cy.getByTestID('input-field').type(label) + cy.getByTestID('inline-labels--create-new').click() + }) - cy.getByTestID('resource-card').should('have.length', 1) + cy.getByTestID('overlay--container').within(() => { + cy.getByInputName('name').should('have.value', label) + cy.getByTestID('create-label--button').click() + }) - cy.getByTestID('resource-card') - .first() - .get('.label') - .should('contain', widgetSearch) + cy.getByTestID('dashboard-card') + .first() + .within(() => { + cy.getByTestID(`label--pill ${label}`).should('be.visible') + }) + }) }) - it('can search by clicking label', () => { - const clicked = 'click-me' + describe('Searching', () => { + it('can search dashboards by labels', () => { + cy.getByTestID('dashboard-card').should('have.length', 2) - cy.get('@org').then(({id}) => { - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, clicked) - }) + cy.getByTestID('search-widget').type(newLabelName) - cy.createDashboard(id).then(({body}) => { - cy.createLabel('dashboards', body.id, 'bar') - }) - }) + cy.getByTestID('dashboard-card').should('have.length', 1) - cy.visit('/dashboards') + cy.getByTestID('dashboard-card') + .first() + .get('.label') + .should('contain', newLabelName) + }) - cy.getByTestID('resource-card').should('have.length', 2) + it('can search by clicking label', () => { + const clicked = 'click-me' - cy.getByTestID(`label--pill ${clicked}`).click() + cy.getByTestID('dashboard-card').should('have.length', 2) - cy.getByTestID('search-widget').should('have.value', clicked) + cy.getByTestID(`label--pill ${clicked}`).click() - cy.getByTestID('resource-card').should('have.length', 1) - }) + cy.getByTestID('search-widget').should('have.value', clicked) - it('can search by dashboard name', () => { - const searchName = 'beepBoop' - cy.get('@org').then(({id}) => { - cy.createDashboard(id, searchName) - cy.createDashboard(id) + cy.getByTestID('dashboard-card').should('have.length', 1) }) - cy.visit('/dashboards') - - cy.getByTestID('search-widget').type('bEE') + it('can search by dashboard name', () => { + cy.getByTestID('search-widget').type(dashSearchName) - cy.getByTestID('resource-card').should('have.length', 1) - cy.getByTestID('dashboard-card--name').contains('span', searchName) + cy.getByTestID('dashboard-card').should('have.length', 1) + cy.getByTestID('dashboard-card--name').contains('span', dashboardName) + }) }) }) }) diff --git a/ui/cypress/e2e/dashboardsView.test.ts b/ui/cypress/e2e/dashboardsView.test.ts index 93108c4f797..3b29a4910d4 100644 --- a/ui/cypress/e2e/dashboardsView.test.ts +++ b/ui/cypress/e2e/dashboardsView.test.ts @@ -29,7 +29,7 @@ describe('Dashboard', () => { cy.visit('/dashboards') - cy.getByTestID('resource-card').should('contain', newName) + cy.getByTestID('dashboard-card').should('contain', newName) }) it('can create a cell', () => { diff --git a/ui/cypress/e2e/tasks.test.ts b/ui/cypress/e2e/tasks.test.ts index bdda7f2c87a..df53bd2cc48 100644 --- a/ui/cypress/e2e/tasks.test.ts +++ b/ui/cypress/e2e/tasks.test.ts @@ -117,11 +117,11 @@ describe('Tasks', () => { cy.get('@org').then(({id}) => { cy.createTask(id).then(({body}) => { - cy.createLabel('tasks', body.id, newLabelName) + cy.createAndAddLabel('tasks', body.id, newLabelName) }) cy.createTask(id).then(({body}) => { - cy.createLabel('tasks', body.id, 'bar') + cy.createAndAddLabel('tasks', body.id, 'bar') }) }) diff --git a/ui/cypress/index.d.ts b/ui/cypress/index.d.ts index 49c55a5ee9a..24d383c63a9 100644 --- a/ui/cypress/index.d.ts +++ b/ui/cypress/index.d.ts @@ -11,6 +11,7 @@ import { getByTitle, createTask, createVariable, + createAndAddLabel, createLabel, createBucket, createScraper, @@ -32,6 +33,7 @@ declare global { getByTestID: typeof getByTestID getByInputName: typeof getByInputName getByTitle: typeof getByTitle + createAndAddLabel: typeof createAndAddLabel createLabel: typeof createLabel createBucket: typeof createBucket createScraper: typeof createScraper diff --git a/ui/cypress/support/commands.ts b/ui/cypress/support/commands.ts index c8ac21bf200..be2eff3f8e1 100644 --- a/ui/cypress/support/commands.ts +++ b/ui/cypress/support/commands.ts @@ -100,6 +100,22 @@ export const createVariable = ( } export const createLabel = ( + name?: string +): Cypress.Chainable => { + return cy.request({ + method: 'POST', + url: '/api/v2/labels', + body: { + name, + properties: { + description: `test ${name}`, + color: '#ff0054', + }, + }, + }) +} + +export const createAndAddLabel = ( resource: string, resourceID: string, name?: string @@ -274,3 +290,4 @@ Cypress.Commands.add('createVariable', createVariable) // Labels Cypress.Commands.add('createLabel', createLabel) +Cypress.Commands.add('createAndAddLabel', createAndAddLabel) diff --git a/ui/src/clockface/components/label/Label.scss b/ui/src/clockface/components/label/Label.scss index 3706a472947..f635dba6d2b 100644 --- a/ui/src/clockface/components/label/Label.scss +++ b/ui/src/clockface/components/label/Label.scss @@ -5,40 +5,38 @@ @import 'src/style/modules'; -$label-margin: 1px; - .label { - display: inline-block; - margin: 0; + display: inline-flex; + align-items: center; + align-content: center; user-select: none; transition: background-color 0.25s ease; + flex-wrap: nowrap; +} + +.label--name { + display: block; + cursor: inherit; + font-weight: 700; white-space: nowrap; - - > label { - cursor: inherit; - font-weight: 700; - margin: 0; - } + margin: 0; } .label--clickable { - &, &:hover { + &, + &:hover { cursor: pointer; } } .label--delete { - z-index: 2; - display: inline-block; + display: block; background-color: transparent; - position: absolute; - top: 0; - right: $ix-border + $label-margin; + position: relative; border: 0; - margin: 0; outline: none; opacity: 0.4; - transition: opacity 0.25s ease; + transition: opacity 0.25s ease, width 0.25s ease, transform 0.25s ease; &:hover { opacity: 1; @@ -53,10 +51,10 @@ $label-margin: 1px; width: 60%; height: $ix-border; border-radius: $ix-border / 2; - transform: translate(-50%,-50%) rotate(45deg); + transform: translate(-50%, -50%) rotate(45deg); &:nth-child(2) { - transform: translate(-50%,-50%) rotate(-45deg); + transform: translate(-50%, -50%) rotate(-45deg); } } @@ -71,18 +69,30 @@ $label-margin: 1px; border-radius: $height / 2; font-size: $fontSize; height: $height - $ix-marg-a; - line-height: $height - $ix-marg-a; + } + + .label--name { padding: 0 ($padding + $ix-border); + height: $height - $ix-marg-a; + line-height: $height - $ix-marg-a; + } + + &.label--deletable { + padding-right: $padding + $ix-border; + } + + &.label--deletable .label--name { + padding-right: 0; } .label--delete { height: $height - $ix-marg-a; - width: $height - $ix-marg-a; + transform: translateX($padding - $ix-border); + width: 0; } - &.label--deletable { - position: relative; - padding-right: $height; + &:hover .label--delete { + width: $height - $ix-marg-a - $ix-border; } } @@ -101,3 +111,14 @@ $label-margin: 1px; .label--lg { @include labelSizeModifier($form-lg-font, $form-lg-padding, $form-lg-height); } + +.label--colorless { + font-weight: 600; + font-style: italic; + + &, + &:hover { + background-color: $g6-smoke; + color: $g13-mist; + } +} diff --git a/ui/src/clockface/components/label/Label.tsx b/ui/src/clockface/components/label/Label.tsx index 0e01c4429cb..8e822f681d5 100644 --- a/ui/src/clockface/components/label/Label.tsx +++ b/ui/src/clockface/components/label/Label.tsx @@ -14,7 +14,7 @@ import './Label.scss' import {ErrorHandling} from 'src/shared/decorators/errors' -export interface LabelType { +interface PassedProps { id: string name: string description: string @@ -23,7 +23,7 @@ export interface LabelType { onDelete?: (id: string) => void } -interface LabelProps { +interface DefaultProps { size?: ComponentSize testID?: string } @@ -32,11 +32,11 @@ interface State { isMouseOver: boolean } -type Props = LabelType & LabelProps +type Props = PassedProps & DefaultProps @ErrorHandling class Label extends Component { - public static defaultProps: Partial = { + public static defaultProps: DefaultProps = { size: ComponentSize.ExtraSmall, testID: 'label--pill', } @@ -59,24 +59,29 @@ class Label extends Component { return (
- + + {name} + {this.deleteButton}
) } private handleClick = (e: MouseEvent): void => { - e.preventDefault() const {id, onClick} = this.props if (onClick) { + e.stopPropagation() + e.preventDefault() onClick(id) } } @@ -110,8 +115,8 @@ class Label extends Component { return classnames('label', { [`label--${size}`]: size, - 'label--clickable': onClick, 'label--deletable': onDelete, + 'label--clickable': onClick, }) } @@ -126,7 +131,7 @@ class Label extends Component { } private get deleteButton(): JSX.Element { - const {onDelete, name} = this.props + const {onDelete, name, testID} = this.props if (onDelete) { return ( @@ -134,7 +139,8 @@ class Label extends Component { className="label--delete" onClick={this.handleDelete} type="button" - title={`Click × to remove "${name}"`} + title={`Remove label "${name}"`} + data-testid={`${testID}--delete ${name}`} >
void - onCreateLabel: (label: LabelType) => void + onCreateLabel: (label: ILabel) => void onNameValidation: (name: string) => string | null overrideDefaultName?: string } interface State { - label: LabelType + label: ILabel useCustomColorHex: boolean } @@ -42,6 +42,18 @@ class CreateLabelOverlay extends Component { useCustomColorHex: false, } + componentDidUpdate(prevProps) { + if ( + prevProps.overrideDefaultName !== this.props.overrideDefaultName && + this.props.isVisible === false + ) { + const name = this.props.overrideDefaultName + const label = {...this.state.label, name} + + this.setState({label}) + } + } + public render() { const {isVisible, onDismiss, onNameValidation} = this.props const {label, useCustomColorHex} = this.state @@ -54,8 +66,8 @@ class CreateLabelOverlay extends Component { { const {label} = this.state const nameIsValid = this.props.onNameValidation(label.name) === null - const colorIsValid = validateHexCode(label.colorHex) === null + const colorIsValid = validateHexCode(label.properties.color) === null return nameIsValid && colorIsValid } @@ -104,7 +116,14 @@ class CreateLabelOverlay extends Component { const value = e.target.value const key = e.target.name - if (key in this.state.label) { + if (key === 'description' || key === 'color') { + const properties = {...this.state.label.properties, [key]: value} + const label = {...this.state.label, properties} + + this.setState({ + label, + }) + } else { const label = {...this.state.label, [key]: value} this.setState({ @@ -113,8 +132,9 @@ class CreateLabelOverlay extends Component { } } - private handleColorHexChange = (colorHex: string): void => { - const label = {...this.state.label, colorHex} + private handleColorHexChange = (color: string): void => { + const properties = {...this.state.label.properties, color} + const label = {...this.state.label, properties} this.setState({label}) } diff --git a/ui/src/configuration/components/LabelList.tsx b/ui/src/configuration/components/LabelList.tsx index d5fabfd81ba..8963793bbf7 100644 --- a/ui/src/configuration/components/LabelList.tsx +++ b/ui/src/configuration/components/LabelList.tsx @@ -10,16 +10,17 @@ import LabelRow from 'src/configuration/components/LabelRow' import {validateLabelUniqueness} from 'src/configuration/utils/labels' // Types -import {LabelType} from 'src/clockface' +import {ILabel} from '@influxdata/influx' import {OverlayState} from 'src/types' // Decorators import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - labels: LabelType[] + labels: ILabel[] emptyState: JSX.Element - onUpdateLabel: (label: LabelType) => void + onUpdateLabel: (label: ILabel) => void + onDeleteLabel: (labelID: string) => void } interface State { @@ -60,19 +61,22 @@ export default class LabelList extends PureComponent { } private get rows(): JSX.Element[] { + const {onDeleteLabel} = this.props + return this.props.labels.map((label, index) => ( )) } - private get label(): LabelType { - return this.props.labels.find(b => b.id === this.state.labelID) + private get label(): ILabel | null { + if (this.state.labelID) { + return this.props.labels.find(l => l.id === this.state.labelID) + } } private handleCloseModal = () => { @@ -88,7 +92,7 @@ export default class LabelList extends PureComponent { return !!labelID && overlayState === OverlayState.Open } - private handleUpdateLabel = async (updatedLabel: LabelType) => { + private handleUpdateLabel = async (updatedLabel: ILabel) => { await this.props.onUpdateLabel(updatedLabel) this.setState({overlayState: OverlayState.Closed}) } @@ -96,7 +100,7 @@ export default class LabelList extends PureComponent { private handleNameValidation = (name: string): string | null => { const {labels} = this.props - const names = labels.map(label => label.name) + const names = labels.map(label => label.name).filter(l => l !== name) return validateLabelUniqueness(names, name) } diff --git a/ui/src/configuration/components/LabelOverlayForm.tsx b/ui/src/configuration/components/LabelOverlayForm.tsx index fd598e1544a..d23845a8c31 100644 --- a/ui/src/configuration/components/LabelOverlayForm.tsx +++ b/ui/src/configuration/components/LabelOverlayForm.tsx @@ -149,6 +149,7 @@ export default class LabelOverlayForm extends PureComponent { text={buttonText} color={ComponentColor.Success} type={ButtonType.Submit} + testID="create-label--button" status={ isFormValid ? ComponentStatus.Default diff --git a/ui/src/configuration/components/LabelRow.tsx b/ui/src/configuration/components/LabelRow.tsx index 8afe608169a..23060a9ca17 100644 --- a/ui/src/configuration/components/LabelRow.tsx +++ b/ui/src/configuration/components/LabelRow.tsx @@ -2,28 +2,25 @@ import React, {PureComponent} from 'react' // Components -import { - Button, - Alignment, - ComponentSize, - ComponentColor, -} from '@influxdata/clockface' -import {IndexList, Label} from 'src/clockface' +import {Alignment, ComponentSize} from '@influxdata/clockface' +import {IndexList, Label, ConfirmationButton} from 'src/clockface' // Types -import {LabelType} from 'src/clockface' +import {ILabel} from '@influxdata/influx' // Decorators import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - label: LabelType + label: ILabel + onClick: (labelID: string) => void + onDelete: (labelID: string) => void } @ErrorHandling export default class LabelRow extends PureComponent { public render() { - const {label} = this.props + const {label, onDelete} = this.props return ( @@ -31,28 +28,29 @@ export default class LabelRow extends PureComponent {