Introduce ResourceList components and implement on Dashboards index (#12166)

* WIP Introduce Resource List component family

* Shrink padding of resource cards a bit

* Swap positions of meta information and labels in resource cards

* Introduce resource name component

* Polish resource name editing

* Remove child type validation from context component

* Styles for context menus inside resource cards

* Make resource name + meta line responsive

* Polish appearance of responsive resource description

* Replace dashboards list with dashboards cards

* Update e2e tests and add testID props to a bunch of components

Co-Authored-By: Andrew Watkins <121watts@users.noreply.github.com>

* Make testID props consistent

Make all cypress tests have .test extension
Update E2E tests for dashboards index
Split off test for dashboards view

Co-Authored-By: Andrew Watkins <121watts@users.noreply.github.com>

* Move cell test to dashboards view test

Co-Authored-By: Andrew Watkins <121watts@users.noreply.github.com>

* Remove cells test from dashboards index test

Co-Authored-By: Andrew Watkins <121watts@users.noreply.github.com>

* Fix dashboard view - cells e2e test

* Refactor meta1 and meta2 props into a single metadata prop that returns an array of elements

* Cleanup

* Fix and refactor e2e test to be less brittle
pull/12152/head
alexpaxton 2019-02-25 17:41:18 -08:00 committed by GitHub
parent 70a54e1f4c
commit 6e1ee40f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1530 additions and 276 deletions

View File

@ -22,17 +22,17 @@ describe('Buckets', () => {
describe('from the org view', () => {
it('can create a bucket', () => {
const newBucket = '🅱ucket'
cy.getByDataTest('table-row').should('have.length', 1)
cy.getByTestID('table-row').should('have.length', 1)
cy.contains('Create').click()
cy.getByDataTest('overlay--container').within(() => {
cy.getByTestID('overlay--container').within(() => {
cy.getByInputName('name').type(newBucket)
cy.get('.button')
.contains('Create')
.click()
})
cy.getByDataTest('table-row')
cy.getByTestID('table-row')
.should('have.length', 2)
.and('contain', newBucket)
})
@ -44,14 +44,14 @@ describe('Buckets', () => {
cy.contains(name).click()
})
cy.getByDataTest('retention-intervals').click()
cy.getByTestID('retention-intervals').click()
cy.getByInputName('days').type('{uparrow}')
cy.getByInputName('hours').type('{uparrow}')
cy.getByInputName('minutes').type('{uparrow}')
cy.getByInputName('seconds').type('{uparrow}')
cy.getByDataTest('overlay--container').within(() => {
cy.getByTestID('overlay--container').within(() => {
cy.getByInputName('name')
.clear()
.type(newName)
@ -59,7 +59,7 @@ describe('Buckets', () => {
cy.contains('Save').click()
})
cy.getByDataTest('table-row')
cy.getByTestID('table-row')
.should('contain', '1 day')
.and('contain', newName)
})

View File

@ -1,101 +0,0 @@
import {Organization} from '@influxdata/influx'
describe('Dashboards', () => {
beforeEach(() => {
cy.flush()
cy.setupUser().then(({body}) => {
cy.wrap(body.org).as('org')
})
cy.get<Organization>('@org').then(org => {
cy.signin(org.id)
})
cy.fixture('routes').then(({dashboards}) => {
cy.visit(dashboards)
})
})
it('can create a dashboard from empty state', () => {
cy.get('.empty-state')
.contains('Create')
.click()
cy.visit('/dashboards')
cy.get('.index-list--row')
.its('length')
.should('be.eq', 1)
})
it('can create a dashboard from the header', () => {
cy.get('.page-header--container')
.contains('Create')
.click()
cy.getByDataTest('dropdown--item New Dashboard').click()
cy.visit('/dashboards')
cy.get('.index-list--row')
.its('length')
.should('be.eq', 1)
})
it('can delete a dashboard', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id)
cy.createDashboard(id)
})
cy.get('.index-list--row').then(rows => {
const numDashboards = rows.length
cy.get('.button-danger')
.first()
.click()
cy.contains('Confirm')
.first()
.click({force: true})
cy.get('.index-list--row')
.its('length')
.should('eq', numDashboards - 1)
})
})
it('can edit a dashboards name', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id).then(({body}) => {
cy.visit(`/dashboards/${body.id}`)
})
})
const newName = 'new 🅱ashboard'
cy.get('.renamable-page-title--title').click()
cy.get('.input-field')
.type(newName)
.type('{enter}')
cy.visit('/dashboards')
cy.get('.index-list--row').should('contain', newName)
})
describe('Cells', () => {
it('can create a cell', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id).then(({body}) => {
cy.visit(`/dashboards/${body.id}`)
})
})
cy.getByDataTest('add-cell--button').click()
cy.getByDataTest('save-cell--button').click()
cy.getByDataTest('cell--view-empty').should('have.length', 1)
})
})
})

View File

@ -0,0 +1,91 @@
import {Organization} from '@influxdata/influx'
describe('Dashboards', () => {
beforeEach(() => {
cy.flush()
cy.setupUser().then(({body}) => {
cy.wrap(body.org).as('org')
})
cy.get<Organization>('@org').then(org => {
cy.signin(org.id)
})
cy.fixture('routes').then(({dashboards}) => {
cy.visit(dashboards)
})
})
it('can create a dashboard from empty state', () => {
cy.getByTestID('empty-state')
.contains('Create')
.click()
cy.visit('/dashboards')
cy.getByTestID('resource-card')
.its('length')
.should('be.eq', 1)
})
it('can create a dashboard from the header', () => {
cy.get('.page-header--container')
.contains('Create')
.click()
cy.getByTestID('dropdown--item New Dashboard').click()
cy.visit('/dashboards')
cy.getByTestID('resource-card')
.its('length')
.should('be.eq', 1)
})
it('can delete a dashboard', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id)
cy.createDashboard(id)
})
cy.getByTestID('resource-card')
.its('length')
.should('eq', 2)
cy.getByTestID('resource-card')
.first()
.trigger('mouseover')
.within(() => {
cy.getByTestID('context-delete-menu').click()
cy.getByTestID('context-delete-dashboard').click()
})
cy.getByTestID('resource-card')
.its('length')
.should('eq', 1)
})
it('can edit a dashboards name', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id)
})
const newName = 'new 🅱ashboard'
cy.getByTestID('resource-card').within(() => {
cy.getByTestID('dashboard-card--name').trigger('mouseover')
cy.getByTestID('dashboard-card--name-button').click()
cy.get('.input-field')
.type(newName)
.type('{enter}')
})
cy.visit('/dashboards')
cy.getByTestID('resource-card').should('contain', newName)
})
})

View File

@ -0,0 +1,50 @@
import {Organization} from '@influxdata/influx'
describe('Dashboard', () => {
beforeEach(() => {
cy.flush()
cy.setupUser().then(({body}) => {
cy.wrap(body.org).as('org')
})
cy.get<Organization>('@org').then(org => {
cy.signin(org.id)
})
cy.fixture('routes').then(({dashboards}) => {
cy.visit(dashboards)
})
})
it('can edit a dashboards name', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id).then(({body}) => {
cy.visit(`/dashboards/${body.id}`)
})
})
const newName = 'new 🅱ashboard'
cy.get('.renamable-page-title--title').click()
cy.get('.input-field')
.type(newName)
.type('{enter}')
cy.visit('/dashboards')
cy.getByTestID('resource-card').should('contain', newName)
})
it('can create a cell', () => {
cy.get<Organization>('@org').then(({id}) => {
cy.createDashboard(id).then(({body}) => {
cy.visit(`/dashboards/${body.id}`)
})
})
cy.getByTestID('add-cell--button').click()
cy.getByTestID('save-cell--button').click()
cy.getByTestID('cell--view-empty').should('have.length', 1)
})
})

View File

@ -22,7 +22,7 @@ describe('The Login Page', () => {
cy.getByInputName('password').type(user.password)
cy.get('button[type=submit]').click()
cy.getByDataTest('nav').should('exist')
cy.getByTestID('nav').should('exist')
})
describe('login failure', () => {
@ -30,14 +30,14 @@ describe('The Login Page', () => {
cy.getByInputName('password').type(user.password)
cy.get('button[type=submit]').click()
cy.getByDataTest('notification-error').should('exist')
cy.getByTestID('notification-error').should('exist')
})
it('if password is not present', () => {
cy.getByInputName('username').type(user.username)
cy.get('button[type=submit]').click()
cy.getByDataTest('notification-error').should('exist')
cy.getByTestID('notification-error').should('exist')
})
it('if username is incorrect', () => {
@ -45,7 +45,7 @@ describe('The Login Page', () => {
cy.getByInputName('password').type(user.password)
cy.get('button[type=submit]').click()
cy.getByDataTest('notification-error').should('exist')
cy.getByTestID('notification-error').should('exist')
})
it('if password is incorrect', () => {
@ -53,7 +53,7 @@ describe('The Login Page', () => {
cy.getByInputName('password').type('not-a-password')
cy.get('button[type=submit]').click()
cy.getByDataTest('notification-error').should('exist')
cy.getByTestID('notification-error').should('exist')
})
})
})

View File

@ -16,14 +16,13 @@ describe('Orgs', () => {
.its('length')
.should('be.eq', 1)
cy.get('.page-header--right > .button')
.contains('Create')
.click()
cy.getByTestID('create-org-button').click()
const orgName = '🅱organization'
cy.getByInputName('name').type(orgName)
cy.getByTitle('Create').click()
cy.getByTestID('create-org-name-input').type(orgName)
cy.getByTestID('create-org-submit-button').click()
cy.get('.index-list--row')
.should('contain', orgName)

View File

@ -22,7 +22,7 @@ describe('Tasks', () => {
cy.getByInputName('interval').type('1d')
cy.getByInputName('offset').type('20m')
cy.getByDataTest('flux-editor').within(() => {
cy.getByTestID('flux-editor').within(() => {
cy.get('textarea').type(
`from(bucket: "defbuck")
|> range(start: -2m)`,
@ -32,7 +32,7 @@ describe('Tasks', () => {
cy.contains('Save').click()
cy.getByDataTest('task-row')
cy.getByTestID('task-row')
.should('have.length', 1)
.and('contain', taskName)
})
@ -43,13 +43,13 @@ describe('Tasks', () => {
cy.createTask(id)
})
cy.getByDataTest('task-row').should('have.length', 2)
cy.getByTestID('task-row').should('have.length', 2)
cy.getByDataTest('confirmation-button')
cy.getByTestID('confirmation-button')
.first()
.click({force: true})
cy.getByDataTest('task-row').should('have.length', 1)
cy.getByTestID('task-row').should('have.length', 1)
})
it('fails to create a task without a valid script', () => {
@ -61,12 +61,12 @@ describe('Tasks', () => {
cy.getByInputName('interval').type('1d')
cy.getByInputName('offset').type('20m')
cy.getByDataTest('flux-editor').within(() => {
cy.getByTestID('flux-editor').within(() => {
cy.get('textarea').type('{}', {force: true})
})
cy.contains('Save').click()
cy.getByDataTest('notification-error').should('exist')
cy.getByTestID('notification-error').should('exist')
})
})

View File

@ -16,7 +16,7 @@ describe('Variables', () => {
})
cy.getByInputName('name').type('Little Variable')
cy.getByDataTest('flux-editor').within(() => {
cy.getByTestID('flux-editor').within(() => {
cy.get('textarea').type('filter(fn: (r) => r._field == "cpu")', {
force: true,
})
@ -26,6 +26,6 @@ describe('Variables', () => {
.contains('Create')
.click()
cy.getByDataTest('variable-row').should('have.length', 1)
cy.getByTestID('variable-row').should('have.length', 1)
})
})

View File

@ -6,7 +6,7 @@ import {
createOrg,
createSource,
flush,
getByDataTest,
getByTestID,
getByInputName,
getByTitle,
createTask,
@ -22,7 +22,7 @@ declare global {
createDashboard: typeof createDashboard
createOrg: typeof createOrg
flush: typeof flush
getByDataTest: typeof getByDataTest
getByTestID: typeof getByTestID
getByInputName: typeof getByInputName
getByTitle: typeof getByTitle
}

View File

@ -98,7 +98,7 @@ export const flush = () => {
}
// DOM node getters
export const getByDataTest = (dataTest: string): Cypress.Chainable => {
export const getByTestID = (dataTest: string): Cypress.Chainable => {
return cy.get(`[data-testid="${dataTest}"]`)
}
@ -111,7 +111,7 @@ export const getByTitle = (name: string): Cypress.Chainable => {
}
// getters
Cypress.Commands.add('getByDataTest', getByDataTest)
Cypress.Commands.add('getByTestID', getByTestID)
Cypress.Commands.add('getByInputName', getByInputName)
Cypress.Commands.add('getByTitle', getByTitle)

View File

@ -1,5 +1,5 @@
import React, {SFC} from 'react'
const MockChild: SFC = () => <div data-test="mock-child" />
const MockChild: SFC = () => <div data-testid="mock-child" />
export default MockChild

5
ui/package-lock.json generated
View File

@ -10500,6 +10500,11 @@
"xml": "^1.0.0"
}
},
"mocks": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/mocks/-/mocks-0.0.15.tgz",
"integrity": "sha1-RmClzFn07Fbrk7/XLwg0+PxPoRw="
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",

View File

@ -91,7 +91,6 @@ class ConfirmationButton extends Component<Props, State> {
/>
<div className={this.tooltipClassName}>
<div
data-test="confirmation-button--click-target"
data-testid={testID}
className="confirmation-button--tooltip-body"
onClick={this.handleTooltipClick}

View File

@ -44,9 +44,7 @@ describe('ConfirmationButton', () => {
wrapper.find(Button).simulate('click')
wrapper
.find({'data-test': 'confirmation-button--click-target'})
.simulate('click')
wrapper.find({'data-testid': 'confirmation-button'}).simulate('click')
expect(onConfirm.mock.results[0].value).toBe(returnValue)
})

View File

@ -44,7 +44,6 @@ exports[`ConfirmationButton interaction shows the tooltip when clicked 1`] = `
>
<div
className="confirmation-button--tooltip-body"
data-test="confirmation-button--click-target"
data-testid="confirmation-button"
onClick={[Function]}
>

View File

@ -55,7 +55,7 @@ class Context extends PureComponent<Props, State> {
/>
)
} else {
throw new Error('Expected children of type <Context.Menu />')
return child
}
})}
</div>

View File

@ -17,26 +17,32 @@ import {
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
interface PassedProps {
children: JSX.Element | JSX.Element[]
icon: IconFont
text?: string
title
color?: ComponentColor
shape?: ButtonShape
onBoostZIndex?: (boostZIndex: boolean) => void
}
interface DefaultProps {
text?: string
color?: ComponentColor
shape?: ButtonShape
testID?: string
}
type Props = PassedProps & DefaultProps
interface State {
isExpanded: boolean
}
@ErrorHandling
class ContextMenu extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
public static defaultProps: DefaultProps = {
color: ComponentColor.Primary,
shape: ButtonShape.Square,
text: '',
testID: 'context-menu',
}
constructor(props: Props) {
@ -48,7 +54,7 @@ class ContextMenu extends Component<Props, State> {
}
public render() {
const {icon, text, shape, color} = this.props
const {icon, text, shape, color, testID} = this.props
return (
<ClickOutside onClickOutside={this.handleCollapseMenu}>
@ -61,6 +67,7 @@ class ContextMenu extends Component<Props, State> {
icon={icon}
size={ComponentSize.ExtraSmall}
color={color}
testID={testID}
/>
{this.menu}
</div>

View File

@ -2,24 +2,36 @@
import React, {Component} from 'react'
import classnames from 'classnames'
interface Props {
interface PassedProps {
label: string
description?: string
action: (value?: any) => void
value?: any
onCollapseMenu?: () => void
disabled?: boolean
}
interface DefaultProps {
description?: string
testID?: string
}
type Props = PassedProps & DefaultProps
class ContextMenuItem extends Component<Props> {
public static defaultProps: DefaultProps = {
description: null,
testID: 'context-menu-item',
}
public render() {
const {label, disabled} = this.props
const {label, disabled, testID} = this.props
return (
<button
className={this.className}
onClick={this.handleClick}
disabled={disabled}
data-testid={testID}
>
{label}
{this.description}

View File

@ -183,7 +183,7 @@ class MultiSelectDropdown extends Component<Props, State> {
autoHeight={true}
maxHeight={maxMenuHeight}
>
<div className="dropdown--menu" data-test="dropdown-menu">
<div className="dropdown--menu" data-testid="dropdown-menu">
{React.Children.map(children, (child: JSX.Element) => {
if (this.childTypeIsValid(child)) {
if (child.type === DropdownItem) {

View File

@ -60,7 +60,7 @@ describe('MultiSelectDropdown', () => {
button.simulate('click')
expect(wrapper.find('[data-test="dropdown-menu"]')).toHaveLength(1)
expect(wrapper.find('[data-testid="dropdown-menu"]')).toHaveLength(1)
})
it('matches snapshot', () => {

View File

@ -64,7 +64,7 @@ exports[`MultiSelectDropdown with menu expanded matches snapshot 1`] = `
>
<div
className="dropdown--menu"
data-test="dropdown-menu"
data-testid="dropdown-menu"
>
<DropdownItem
checkbox={true}

View File

@ -14,26 +14,37 @@ import 'src/clockface/components/empty_state/EmptyState.scss'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
size?: ComponentSize
interface PassedProps {
children: JSX.Element | JSX.Element[]
}
interface DefaultProps {
size?: ComponentSize
testID?: string
}
type Props = PassedProps & DefaultProps
@ErrorHandling
class EmptyState extends Component<Props> {
public static defaultProps: Partial<Props> = {
public static defaultProps: DefaultProps = {
size: ComponentSize.Small,
testID: 'empty-state',
}
public static Text = EmptyStateText
public static SubText = EmptyStateSubText
public render() {
const {children, size} = this.props
const {children, size, testID} = this.props
const className = `empty-state empty-state--${size}`
return <div className={className}>{children}</div>
return (
<div className={className} data-testid={testID}>
{children}
</div>
)
}
}

View File

@ -23,7 +23,7 @@ class IndexListBody extends Component<Props> {
<tbody className="index-list--empty">
<tr className="index-list--empty-row">
<td colSpan={columnCount}>
<div className="index-list--empty-cell" data-test="empty-state">
<div className="index-list--empty-cell" data-testid="empty-state">
{emptyState}
</div>
</td>

View File

@ -69,9 +69,9 @@ describe('IndexList', () => {
const emptyDiv = wrapper
.find('div')
.filterWhere(div => div.prop('data-test'))
.filterWhere(div => div.prop('data-testid'))
expect(emptyDiv.prop('data-test')).toBe('empty-state')
expect(emptyDiv.prop('data-testid')).toBe('empty-state')
})
it('matches snapshot when 0 rows exist', () => {

View File

@ -69,7 +69,7 @@ exports[`IndexList matches snapshot when 0 rows exist 1`] = `
>
<div
className="index-list--empty-cell"
data-test="empty-state"
data-testid="empty-state"
>
<div>
Empty

View File

@ -22,37 +22,43 @@ export enum AutoComplete {
Off = 'off',
}
interface Props {
interface PassedProps {
id?: string
min?: number
max?: number
name?: string
value: string | number
placeholder?: string
autocomplete?: AutoComplete
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
onBlur?: (e?: ChangeEvent<HTMLInputElement>) => void
onFocus?: (e?: ChangeEvent<HTMLInputElement>) => void
onKeyPress?: (e: KeyboardEvent<HTMLInputElement>) => void
onKeyUp?: (e: KeyboardEvent<HTMLInputElement>) => void
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
size?: ComponentSize
icon?: IconFont
status?: ComponentStatus
autoFocus?: boolean
spellCheck?: boolean
type: InputType
widthPixels?: number
titleText?: string
disabledTitleText?: string
customClass?: string
maxLength?: number
tabIndex?: number
dataTest?: string
}
interface DefaultProps {
type?: InputType
name?: string
value: string | number
placeholder?: string
titleText?: string
autocomplete?: AutoComplete
disabledTitleText?: string
size?: ComponentSize
status?: ComponentStatus
autoFocus?: boolean
spellCheck?: boolean
testID?: string
}
type Props = PassedProps & DefaultProps
class Input extends Component<Props> {
public static defaultProps: Partial<Props> = {
public static defaultProps: DefaultProps = {
type: InputType.Text,
name: '',
value: '',
placeholder: '',
@ -63,6 +69,7 @@ class Input extends Component<Props> {
status: ComponentStatus.Default,
autoFocus: false,
spellCheck: false,
testID: 'input-field',
}
public render() {
@ -85,7 +92,7 @@ class Input extends Component<Props> {
maxLength,
autocomplete,
tabIndex,
dataTest,
testID,
} = this.props
return (
@ -112,7 +119,7 @@ class Input extends Component<Props> {
disabled={status === ComponentStatus.Disabled}
maxLength={maxLength}
tabIndex={tabIndex}
data-test={dataTest}
data-testid={testID}
/>
{this.icon}
{this.statusIndicator}
@ -170,7 +177,7 @@ class Input extends Component<Props> {
<span
className={`input-status icon ${
IconFont.AlertTriangle
} data-test="input-error"`}
} data-testid="input-error"`}
/>
<div className="input-shadow" />
</>
@ -183,7 +190,7 @@ class Input extends Component<Props> {
<span
className={`input-status icon ${
IconFont.Checkmark
} data-test="input-valid"`}
} data-testid="input-valid"`}
/>
<div className="input-shadow" />
</>

View File

@ -55,13 +55,13 @@ class OverlayTechnology extends Component<Props, State> {
if (showChildren) {
return (
<div className="overlay--dialog" data-test="overlay-children">
<div className="overlay--dialog" data-testid="overlay-children">
{children}
</div>
)
}
return <div className="overlay--dialog" data-test="overlay-children" />
return <div className="overlay--dialog" data-testid="overlay-children" />
}
private get overlayClass(): string {

View File

@ -0,0 +1,126 @@
// Libraries
import React, {PureComponent} from 'react'
import {Link} from 'react-router'
import moment from 'moment'
// Types
import {Organization} from 'src/types/v2'
// Constants
import {UPDATED_AT_TIME_FORMAT} from 'src/dashboards/constants'
interface PassedProps {
name: () => JSX.Element
description?: () => JSX.Element
updatedAt?: string
owner?: Organization
labels?: () => JSX.Element
metaData?: () => JSX.Element[]
contextMenu?: () => JSX.Element
children?: JSX.Element[] | JSX.Element
}
interface DefaultProps {
testID?: string
}
type Props = PassedProps & DefaultProps
export default class ResourceListCard extends PureComponent<Props> {
public static defaultProps: DefaultProps = {
testID: 'resource-card',
}
public render() {
const {description, children, testID, labels} = this.props
return (
<div className="resource-list--card" data-testid={testID}>
{this.nameAndMeta}
{description()}
{labels()}
{children}
{this.contextMenu}
</div>
)
}
private get nameAndMeta(): JSX.Element {
const {name} = this.props
return (
<div className="resource-list--name-meta">
{name()}
{this.formattedMetaData}
</div>
)
}
private get formattedMetaData(): JSX.Element {
const {updatedAt, owner, metaData} = this.props
if (!updatedAt && !owner && !metaData) {
return null
}
return (
<div className="resource-list--meta">
{this.ownerLink}
{this.updated}
{this.metaData}
</div>
)
}
private get updated(): JSX.Element {
const {updatedAt} = this.props
if (updatedAt) {
const relativeTimestamp = moment(updatedAt).fromNow()
const absoluteTimestamp = moment(updatedAt).format(UPDATED_AT_TIME_FORMAT)
return (
<div className="resource-list--meta-item" title={absoluteTimestamp}>
{`Modified ${relativeTimestamp}`}
</div>
)
}
}
private get ownerLink(): JSX.Element {
const {owner} = this.props
if (owner) {
return (
<div className="resource-list--meta-item">
<Link
to={`/organizations/${owner.id}/members_tab`}
className="resource-list--owner"
>
{owner.name}
</Link>
</div>
)
}
}
private get metaData(): JSX.Element[] {
const {metaData} = this.props
if (metaData) {
return React.Children.map(metaData(), (m: JSX.Element) => {
if (m !== null && m !== undefined) {
return <div className="resource-list--meta-item">{m}</div>
}
})
}
}
private get contextMenu(): JSX.Element {
const {contextMenu} = this.props
if (contextMenu) {
return <div className="resource-list--context-menu">{contextMenu()}</div>
}
}
}

View File

@ -0,0 +1,77 @@
@import 'src/style/modules';
/*
Resource Editable Description
------------------------------------------------------------------------------
*/
.resource-description {
width: 100%;
min-height: $form-xs-height;
}
.resource-description--preview,
.input.resource-description--input > input {
font-size: $form-xs-font;
font-weight: 500;
font-family: $ix-text-font;
}
.resource-description--preview,
.resource-description--input {
position: relative;
width: 100%;
}
.resource-description--preview {
width: auto;
display: inline-block;
border-radius: $radius;
position: relative;
overflow: hidden;
@include no-user-select();
color: $g13-mist;
transition: color 0.25s ease, background-color 0.25s ease,
border-color 0.25s ease;
line-height: $form-xs-font + $ix-border;
.icon {
position: relative;
top: -2px;
display: inline-block;
margin-left: $ix-marg-b;
opacity: 0;
transition: opacity 0.25s ease;
color: $g11-sidewalk;
}
&:hover .icon {
opacity: 1;
}
&.untitled {
color: $g9-mountain;
font-style: italic;
}
&:hover {
cursor: text;
color: $g20-white;
}
}
/* Ensure placeholder text matches font weight of title */
.input.resource-description--input > input {
&::-webkit-input-placeholder {
font-weight: $page-title-weight !important;
}
&::-moz-placeholder {
font-weight: $page-title-weight !important;
}
&:-ms-input-placeholder {
font-weight: $page-title-weight !important;
}
&:-moz-placeholder {
font-weight: $page-title-weight !important;
}
}

View File

@ -0,0 +1,129 @@
// Libraries
import React, {Component, KeyboardEvent, ChangeEvent} from 'react'
import classnames from 'classnames'
// Components
import {Input, ComponentSize} from 'src/clockface'
import {ClickOutside} from 'src/shared/components/ClickOutside'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
// Styles
import 'src/clockface/components/resource_list/ResourceDescription.scss'
interface Props {
onUpdate: (name: string) => void
description: string
placeholder?: string
}
interface State {
isEditing: boolean
workingDescription: string
}
@ErrorHandling
class ResourceDescription extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isEditing: false,
workingDescription: props.description,
}
}
public render() {
const {description} = this.props
const {isEditing} = this.state
if (isEditing) {
return (
<div className="resource-description">
<ClickOutside onClickOutside={this.handleStopEditing}>
{this.input}
</ClickOutside>
</div>
)
}
return (
<div className="resource-description">
<div
className={this.previewClassName}
onClick={this.handleStartEditing}
>
{description || 'No description'}
<span className="icon pencil" />
</div>
</div>
)
}
private get input(): JSX.Element {
const {placeholder} = this.props
const {workingDescription} = this.state
return (
<Input
size={ComponentSize.ExtraSmall}
autoFocus={true}
spellCheck={false}
placeholder={placeholder}
onFocus={this.handleInputFocus}
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
customClass="resource-description--input"
value={workingDescription}
/>
)
}
private handleStartEditing = (): void => {
this.setState({isEditing: true})
}
private handleStopEditing = async (): Promise<void> => {
const {workingDescription} = this.state
const {onUpdate} = this.props
await onUpdate(workingDescription)
this.setState({isEditing: false})
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({workingDescription: e.target.value})
}
private handleKeyDown = async (
e: KeyboardEvent<HTMLInputElement>
): Promise<void> => {
const {onUpdate, description} = this.props
const {workingDescription} = this.state
if (e.key === 'Enter') {
await onUpdate(workingDescription)
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
this.setState({isEditing: false, workingDescription: description})
}
}
private handleInputFocus = (e: ChangeEvent<HTMLInputElement>): void => {
e.currentTarget.select()
}
private get previewClassName(): string {
const {description} = this.props
return classnames('resource-description--preview', {
untitled: !description,
})
}
}
export default ResourceDescription

View File

@ -0,0 +1,204 @@
@import 'src/style/modules';
/*
Resource Lists
------------------------------------------------------------------------------
Hence the name these are used to display lists of resources and starndardize
the appearance and UX of managing resources
*/
.resource-list {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
}
/*
Header & Sorting
------------------------------------------------------------------------------
*/
.resource-list--header {
display: flex;
align-items: center;
padding-bottom: $ix-marg-c;
justify-content: space-between;
}
.resource-list--sorting {
display: flex;
align-items: center;
}
.resource-list--filter {
min-width: 10px;
}
.resource-list--sorter {
user-select: none;
font-size: $form-md-font;
font-weight: 600;
text-transform: uppercase;
color: $g11-sidewalk;
transition: color 0.25s ease;
margin-left: $ix-marg-c;
display: flex;
align-items: center;
align-content: center;
&:hover {
color: $g18-cloud;
cursor: pointer;
}
&.resource-list--sort-descending,
&.resource-list--sort-ascending {
color: $c-pool;
}
}
.resource-list--sort-arrow {
width: $ix-marg-c;
height: $ix-marg-c;
position: relative;
margin-left: $ix-marg-a;
transition: opacity 0.25s ease;
opacity: 0;
.resource-list--sort-descending &,
.resource-list--sort-ascending & {
opacity: 1;
}
> span.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: transform 0.25s ease;
}
.resource-list--sort-descending & > span.icon {
transform: translate(-50%, -50%) rotate(0deg);
}
.resource-list--sort-ascending & > span.icon {
transform: translate(-50%, -50%) rotate(180deg);
}
}
/*
Cards
------------------------------------------------------------------------------
*/
.resource-list--card {
position: relative;
color: $g13-mist;
background-color: $g3-castle;
border-radius: $radius;
padding: $ix-marg-b $ix-marg-c;
margin-bottom: $ix-border;
transition: background-color 0.25s ease, color 0.25s ease;
display: flex;
flex-direction: column;
&:hover {
color: $g16-pearl;
background-color: $g4-onyx;
}
> * {
margin-bottom: $ix-border;
&:last-child {
margin-bottom: 0;
}
}
}
.resource-list--meta {
display: inline-flex;
font-size: $form-xs-font;
font-weight: 600;
color: $g11-sidewalk;
}
.resource-list--meta-item {
transition: border-color 0.25s ease, color 0.25s ease;
border-right: $ix-border solid $g5-pepper;
padding-right: $ix-marg-b;
margin-right: $ix-marg-b;
&:last-child {
border-right: 0;
padding-right: 0;
margin-right: 0;
}
.resource-list--card:hover & {
color: $g15-platinum;
border-color: $g7-graphite;
}
}
.resource-list--card a.resource-list--owner {
color: $g11-sidewalk !important;
}
.resource-list--card:hover a.resource-list--owner {
color: $g15-platinum !important;
}
.resource-list--card:hover a.resource-list--owner:hover {
color: $c-laser !important;
}
.resource-list--context-menu {
opacity: 0;
transition: opacity 0.25s ease;
position: absolute;
top: $ix-marg-b;
right: $ix-marg-b;
.resource-list--card:hover & {
opacity: 1;
}
}
.resource-list--name-meta {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-wrap: nowrap;
> *:first-child {
margin-bottom: $ix-marg-a;
}
}
@media screen and (min-width: 767px) {
.resource-list--name-meta {
flex-direction: row;
align-items: center;
> *:first-child {
margin-bottom: 0;
}
}
}
/*
Depth Styling
------------------------------------------------------------------------------
*/
.panel .resource-list--card,
.tabs .resource-list--card {
background-color: $g4-onyx;
&:hover {
background-color: $g5-pepper;
}
}

View File

@ -0,0 +1,30 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import ResourceListHeader from 'src/clockface/components/resource_list/ResourceListHeader'
import ResourceListSorter from 'src/clockface/components/resource_list/ResourceListSorter'
import ResourceListBody from 'src/clockface/components/resource_list/ResourceListBody'
import ResourceCard from 'src/clockface/components/resource_list/ResourceCard'
import ResourceName from 'src/clockface/components/resource_list/ResourceName'
import ResourceDescription from 'src/clockface/components/resource_list/ResourceDescription'
// Styles
import 'src/clockface/components/resource_list/ResourceList.scss'
interface Props {
children: JSX.Element[] | JSX.Element
}
export default class ResourceList extends PureComponent<Props> {
public static Header = ResourceListHeader
public static Sorter = ResourceListSorter
public static Body = ResourceListBody
public static Card = ResourceCard
public static Name = ResourceName
public static Description = ResourceDescription
public render() {
return <div className="resource-list">{this.props.children}</div>
}
}

View File

@ -0,0 +1,23 @@
// Libraries
import React, {PureComponent} from 'react'
interface Props {
children: JSX.Element[] | JSX.Element
emptyState: JSX.Element
}
export default class ResourceListBody extends PureComponent<Props> {
public render() {
return <div className="resource-list--body">{this.children}</div>
}
private get children(): JSX.Element | JSX.Element[] {
const {children, emptyState} = this.props
if (React.Children.count(children) === 0) {
return emptyState
}
return children
}
}

View File

@ -0,0 +1,30 @@
// Libraries
import React, {PureComponent} from 'react'
interface Props {
children: JSX.Element[] | JSX.Element
filterComponent?: () => JSX.Element
}
export default class ResourceListHeader extends PureComponent<Props> {
public render() {
const {children} = this.props
return (
<div className="resource-list--header">
{this.filter}
<div className="resource-list--sorting">{children}</div>
</div>
)
}
private get filter(): JSX.Element {
const {filterComponent} = this.props
if (filterComponent) {
return <div className="resource-list--filter">{filterComponent()}</div>
}
return <div className="resource-list--filter" />
}
}

View File

@ -0,0 +1,77 @@
// Libraries
import React, {PureComponent} from 'react'
import classnames from 'classnames'
// Types
import {Sort} from 'src/clockface/types'
interface Props {
sortKey: string
sort: Sort
name: string
onClick?: (nextSort: Sort, sortKey: string) => void
}
export default class ResourceListSorter extends PureComponent<Props> {
public render() {
const {name} = this.props
return (
<div
className={this.className}
onClick={this.handleClick}
title={this.title}
>
{name}
{this.sortIndicator}
</div>
)
}
private handleClick = (): void => {
const {onClick, sort, sortKey} = this.props
if (!onClick || !sort) {
return
}
if (sort === Sort.None) {
onClick(Sort.Ascending, sortKey)
} else if (sort === Sort.Ascending) {
onClick(Sort.Descending, sortKey)
} else if (sort === Sort.Descending) {
onClick(Sort.None, sortKey)
}
}
private get title(): string {
const {sort, name} = this.props
if (sort === Sort.None) {
return `Sort ${name} in ${Sort.Ascending} order`
} else if (sort === Sort.Ascending) {
return `Sort ${name} in ${Sort.Descending} order`
}
}
private get sortIndicator(): JSX.Element {
const {onClick} = this.props
if (onClick) {
return (
<span className="resource-list--sort-arrow">
<span className="icon caret-down" />
</span>
)
}
}
private get className(): string {
const {sort} = this.props
return classnames('resource-list--sorter', {
'resource-list--sort-descending': sort === Sort.Descending,
'resource-list--sort-ascending': sort === Sort.Ascending,
})
}
}

View File

@ -0,0 +1,79 @@
@import 'src/style/modules';
/*
Editable Name for Resource Cards
------------------------------------------------------------------------------
*/
$resource-name-font-size: 17px;
$resource-name-font-weight: 600;
.resource-name > a,
.input.resource-name--input > input {
font-size: $resource-name-font-size;
font-weight: $resource-name-font-weight;
font-family: $ix-text-font;
}
.resource-name > a {
display: inline-block;
white-space: nowrap;
line-height: $form-xs-height;
}
.resource-name {
height: $form-xs-height;
position: relative;
display: inline-flex;
flex-wrap: nowrap;
margin-right: $ix-marg-a;
}
.input.resource-name--input {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
/* Ensure placeholder text matches font weight of title */
.input.resource-name--input > input {
&::-webkit-input-placeholder {
font-weight: $resource-name-font-weight !important;
}
&::-moz-placeholder {
font-weight: $resource-name-font-weight !important;
}
&:-ms-input-placeholder {
font-weight: $resource-name-font-weight !important;
}
&:-moz-placeholder {
font-weight: $resource-name-font-weight !important;
}
}
/* Edit button hover behavior */
.resource-name--toggle {
font-size: $resource-name-font-size * 0.75;
transform: translateY(-10%);
padding: $ix-marg-a;
display: inline-block;
margin-left: $ix-marg-b;
transition: color 0.25s ease, opacity 0.25s ease, width 0.25s ease;
opacity: 0;
width: 0;
color: $g11-sidewalk;
&:hover {
cursor: pointer;
color: $g15-platinum;
}
}
.resource-name:hover,
.resource-name--editing {
.resource-name--toggle {
opacity: 1;
width: $ix-marg-b + $ix-marg-c;
}
}

View File

@ -0,0 +1,160 @@
// Libraries
import React, {Component, KeyboardEvent, ChangeEvent, MouseEvent} from 'react'
import classnames from 'classnames'
// Components
import {Input, ComponentSize} from 'src/clockface'
import {ClickOutside} from 'src/shared/components/ClickOutside'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
// Styles
import 'src/clockface/components/resource_list/ResourceName.scss'
interface PassedProps {
onUpdate: (name: string) => void
name: string
onEditName?: (e?: MouseEvent<HTMLAnchorElement>) => void
placeholder?: string
noNameString: string
}
interface DefaultProps {
parentTestID?: string
buttonTestID?: string
inputTestID?: string
hrefValue?: string
}
type Props = PassedProps & DefaultProps
interface State {
isEditing: boolean
workingName: string
}
@ErrorHandling
class ResourceName extends Component<Props, State> {
public static defaultProps: DefaultProps = {
parentTestID: 'resource-name',
buttonTestID: 'resource-name--button',
inputTestID: 'resource-name--input',
hrefValue: '#',
}
constructor(props: Props) {
super(props)
this.state = {
isEditing: false,
workingName: props.name,
}
}
public render() {
const {
name,
onEditName,
hrefValue,
noNameString,
parentTestID,
buttonTestID,
} = this.props
return (
<div className={this.className} data-testid={parentTestID}>
<a href={hrefValue} onClick={onEditName}>
<span>{name || noNameString}</span>
</a>
<div
className="resource-name--toggle"
onClick={this.handleStartEditing}
data-testid={buttonTestID}
>
<span className="icon pencil" />
</div>
{this.input}
</div>
)
}
private get input(): JSX.Element {
const {placeholder, inputTestID} = this.props
const {workingName, isEditing} = this.state
if (isEditing) {
return (
<ClickOutside onClickOutside={this.handleStopEditing}>
<Input
size={ComponentSize.ExtraSmall}
maxLength={90}
autoFocus={true}
spellCheck={false}
placeholder={placeholder}
onFocus={this.handleInputFocus}
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
customClass="resource-name--input"
value={workingName}
testID={inputTestID}
/>
</ClickOutside>
)
}
}
private handleStartEditing = (): void => {
this.setState({isEditing: true})
}
private handleStopEditing = async (): Promise<void> => {
const {workingName} = this.state
const {onUpdate} = this.props
await onUpdate(workingName)
this.setState({isEditing: false})
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({workingName: e.target.value})
}
private handleKeyDown = async (
e: KeyboardEvent<HTMLInputElement>
): Promise<void> => {
const {onUpdate, name} = this.props
const {workingName} = this.state
if (e.key === 'Enter') {
e.persist()
if (!workingName) {
this.setState({isEditing: false, workingName: name})
return
}
await onUpdate(workingName)
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
this.setState({isEditing: false, workingName: name})
}
}
private handleInputFocus = (e: ChangeEvent<HTMLInputElement>): void => {
e.currentTarget.select()
}
private get className(): string {
const {name, noNameString} = this.props
const {isEditing} = this.state
return classnames('resource-name', {
'resource-name--editing': isEditing,
'untitled-name': name === noNameString,
})
}
}
export default ResourceName

View File

@ -22,6 +22,7 @@ import ProgressBar from './components/wizard/ProgressBar'
import ComponentSpacer from './components/component_spacer/ComponentSpacer'
import EmptyState from './components/empty_state/EmptyState'
import IndexList from './components/index_views/IndexList'
import ResourceList from './components/resource_list/ResourceList'
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'
@ -95,6 +96,7 @@ export {
ProgressBar,
QuestionMarkTooltip,
Radio,
ResourceList,
ResponsiveGridSizer,
Select,
Sort,

View File

@ -166,7 +166,7 @@ export default class GraphOptionsCustomizableField extends Component<Props> {
spellCheck={false}
id="internalName"
value={displayName}
data-test="custom-time-format"
data-testid="custom-time-format"
onChange={this.handleFieldRename}
placeholder={`Rename ${internalName}`}
disabled={!visible}

View File

@ -0,0 +1,158 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import {IconFont, ComponentColor} from '@influxdata/clockface'
import {Label, ResourceList, Context} from 'src/clockface'
import FeatureFlag from 'src/shared/components/FeatureFlag'
// Types
import {Dashboard, Organization} from 'src/types/v2'
// Constants
import {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
showOwnerColumn: boolean
}
export default class DashboardCard extends PureComponent<Props> {
public render() {
const {dashboard} = this.props
const {id} = dashboard
return (
<ResourceList.Card
key={`dashboard-id--${id}`}
testID="resource-card"
name={() => (
<ResourceList.Name
onUpdate={this.handleUpdateDashboard}
name={dashboard.name}
hrefValue={`/dashboards/${dashboard.id}`}
noNameString={DEFAULT_DASHBOARD_NAME}
parentTestID="dashboard-card--name"
buttonTestID="dashboard-card--name-button"
inputTestID="dashboard-card--input"
/>
)}
description={() => (
<ResourceList.Description
onUpdate={this.handleUpdateDescription}
description={dashboard.description}
placeholder={`Describe ${dashboard.name}`}
/>
)}
labels={() => this.labels}
updatedAt={dashboard.meta.updatedAt}
owner={this.ownerOrg}
contextMenu={() => this.contextMenu}
/>
)
}
private handleUpdateDashboard = (name: string) => {
this.props.onUpdateDashboard({...this.props.dashboard, name})
}
private get contextMenu(): JSX.Element {
const {
dashboard,
onDeleteDashboard,
onExportDashboard,
onCloneDashboard,
} = this.props
return (
<Context>
<FeatureFlag>
<Context.Menu icon={IconFont.CogThick}>
<Context.Item
label="Export"
action={onExportDashboard}
value={dashboard}
/>
</Context.Menu>
</FeatureFlag>
<Context.Menu
icon={IconFont.Duplicate}
color={ComponentColor.Secondary}
>
<Context.Item
label="Clone"
action={onCloneDashboard}
value={dashboard}
/>
</Context.Menu>
<Context.Menu
icon={IconFont.Trash}
color={ComponentColor.Danger}
testID="context-delete-menu"
>
<Context.Item
label="Delete"
action={onDeleteDashboard}
value={dashboard}
testID="context-delete-dashboard"
/>
</Context.Menu>
</Context>
)
}
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={8}
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}
/>
))}
</Label.Container>
)
}
private handleEditLabels = () => {
const {dashboard, onEditLabels} = this.props
onEditLabels(dashboard)
}
private get ownerOrg(): Organization {
const {dashboard, orgs} = this.props
return orgs.find(o => o.id === dashboard.orgID)
}
private handleUpdateDescription = (description: string): void => {
const {onUpdateDashboard} = this.props
const dashboard = {...this.props.dashboard, description}
onUpdateDashboard(dashboard)
}
}

View File

@ -0,0 +1,48 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import DashboardCard from 'src/dashboards/components/dashboard_index/DashboardCard'
// 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
orgs: Organization[]
showOwnerColumn: boolean
}
export default class DashboardCards extends PureComponent<Props> {
public render() {
const {
dashboards,
onExportDashboard,
onCloneDashboard,
onDeleteDashboard,
onUpdateDashboard,
onEditLabels,
orgs,
showOwnerColumn,
} = this.props
return dashboards.map(d => (
<DashboardCard
key={d.id}
dashboard={d}
onExportDashboard={onExportDashboard}
onCloneDashboard={onCloneDashboard}
onDeleteDashboard={onDeleteDashboard}
onUpdateDashboard={onUpdateDashboard}
onEditLabels={onEditLabels}
orgs={orgs}
showOwnerColumn={showOwnerColumn}
/>
))
}
}

View File

@ -114,10 +114,6 @@ class DashboardIndex extends PureComponent<Props, State> {
<Page.Title title="Dashboards" />
</Page.Header.Left>
<Page.Header.Right>
<SearchWidget
placeholderText="Filter dashboards by name..."
onSearch={this.handleFilterDashboards}
/>
<AddResourceDropdown
onSelectNew={this.handleCreateDashboard}
onSelectImport={this.handleToggleImportOverlay}
@ -128,6 +124,12 @@ class DashboardIndex extends PureComponent<Props, State> {
<Page.Contents fullWidth={false} scrollable={true}>
<div className="col-md-12">
<DashboardsIndexContents
filterComponent={() => (
<SearchWidget
placeholderText="Filter dashboards by name..."
onSearch={this.handleFilterDashboards}
/>
)}
orgs={orgs}
dashboards={dashboards}
onSetDefaultDashboard={this.handleSetDefaultDashboard}

View File

@ -27,6 +27,7 @@ interface Props {
notify: (message: Notification) => void
searchTerm: string
showOwnerColumn: boolean
filterComponent?: () => JSX.Element
}
@ErrorHandling
@ -45,6 +46,7 @@ export default class DashboardsIndexContents extends Component<Props> {
orgs,
showOwnerColumn,
dashboards,
filterComponent,
} = this.props
return (
@ -56,6 +58,7 @@ export default class DashboardsIndexContents extends Component<Props> {
>
{filteredDashboards => (
<Table
filterComponent={filterComponent}
searchTerm={searchTerm}
dashboards={filteredDashboards}
onDeleteDashboard={onDeleteDashboard}

View File

@ -7,22 +7,17 @@ import _ from 'lodash'
import {
Button,
IconFont,
Alignment,
ComponentSize,
ComponentColor,
} from '@influxdata/clockface'
import {EmptyState, IndexList} from 'src/clockface'
import TableRows from 'src/dashboards/components/dashboard_index/TableRows'
import {EmptyState, ResourceList} from 'src/clockface'
import DashboardCards from 'src/dashboards/components/dashboard_index/DashboardCards'
import SortingHat from 'src/shared/components/sorting_hat/SortingHat'
// Types
import {Sort} from 'src/clockface'
import {Dashboard, Organization} from 'src/types/v2'
// Constants
const OWNER_COL_WIDTH = 17
const NAME_COL_WIDTH = 63
interface Props {
searchTerm: string
dashboards: Dashboard[]
@ -36,6 +31,7 @@ interface Props {
onEditLabels: (dashboard: Dashboard) => void
orgs: Organization[]
showOwnerColumn: boolean
filterComponent?: () => JSX.Element
}
interface DatedDashboard extends Dashboard {
@ -59,36 +55,30 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
}
public render() {
const {filterComponent} = this.props
const {sortKey, sortDirection} = this.state
return (
<IndexList>
<IndexList.Header>
<IndexList.HeaderCell
columnName={this.headerKeys[0]}
<ResourceList>
<ResourceList.Header filterComponent={filterComponent}>
<ResourceList.Sorter
name={this.headerKeys[0]}
sortKey={this.headerKeys[0]}
sort={sortKey === this.headerKeys[0] ? sortDirection : Sort.None}
width={this.nameColWidth}
onClick={this.handleClickColumn}
/>
{this.ownerColumnHeader}
<IndexList.HeaderCell
columnName={this.headerKeys[2]}
{this.ownerSorter}
<ResourceList.Sorter
name={this.headerKeys[2]}
sortKey={this.headerKeys[2]}
sort={sortKey === this.headerKeys[2] ? sortDirection : Sort.None}
width="11%"
onClick={this.handleClickColumn}
/>
<IndexList.HeaderCell
columnName=""
width="10%"
alignment={Alignment.Right}
/>
</IndexList.Header>
<IndexList.Body emptyState={this.emptyState} columnCount={5}>
{this.sortedRows}
</IndexList.Body>
</IndexList>
</ResourceList.Header>
<ResourceList.Body emptyState={this.emptyState}>
{this.sortedCards}
</ResourceList.Body>
</ResourceList>
)
}
@ -96,38 +86,27 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
return ['name', 'owner', 'modified', 'default']
}
private get ownerColumnHeader(): JSX.Element {
private get ownerSorter(): JSX.Element {
const {showOwnerColumn} = this.props
const {sortKey, sortDirection} = this.state
if (showOwnerColumn) {
return (
<IndexList.HeaderCell
columnName={this.headerKeys[1]}
<ResourceList.Sorter
name={this.headerKeys[1]}
sortKey={this.headerKeys[1]}
sort={sortKey === this.headerKeys[1] ? sortDirection : Sort.None}
width={`${OWNER_COL_WIDTH}%`}
onClick={this.handleClickColumn}
/>
)
}
}
private get nameColWidth(): string {
const {showOwnerColumn} = this.props
if (showOwnerColumn) {
return `${NAME_COL_WIDTH}%`
}
return `${NAME_COL_WIDTH + OWNER_COL_WIDTH}%`
}
private handleClickColumn = (nextSort: Sort, sortKey: SortKey) => {
this.setState({sortKey, sortDirection: nextSort})
}
private get sortedRows(): JSX.Element {
private get sortedCards(): JSX.Element {
const {
dashboards,
onExportDashboard,
@ -149,7 +128,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
direction={sortDirection}
>
{ds => (
<TableRows
<DashboardCards
dashboards={ds}
onCloneDashboard={onCloneDashboard}
onExportDashboard={onExportDashboard}

View File

@ -59,7 +59,7 @@ class SaveAsButton extends PureComponent<Props, State> {
active={saveAsOption === SaveAsOption.Dashboard}
value={SaveAsOption.Dashboard}
onClick={this.handleSetSaveAsOption}
data-test="cell-radio-button"
data-testid="cell-radio-button"
>
Dashboard Cell
</Radio.Button>
@ -67,7 +67,7 @@ class SaveAsButton extends PureComponent<Props, State> {
active={saveAsOption === SaveAsOption.Task}
value={SaveAsOption.Task}
onClick={this.handleSetSaveAsOption}
data-test="task-radio-button"
data-testid="task-radio-button"
>
Task
</Radio.Button>

View File

@ -36,7 +36,7 @@ exports[`SaveAsButton rendering renders 1`] = `
>
<RadioButton
active={true}
data-test="cell-radio-button"
data-testid="cell-radio-button"
disabled={false}
disabledTitleText="This option is disabled"
onClick={[Function]}
@ -47,7 +47,7 @@ exports[`SaveAsButton rendering renders 1`] = `
</RadioButton>
<RadioButton
active={false}
data-test="task-radio-button"
data-testid="task-radio-button"
disabled={false}
disabledTitleText="This option is disabled"
onClick={[Function]}

View File

@ -42,7 +42,7 @@ describe('DataLoaders.Components.CollectorsWizard.Configure.ConfigFieldHandler',
telegrafPluginsInfo[TelegrafPluginInputCpu.NameEnum.Cpu].fields,
})
const noConfig = wrapper.find({
'data-test': 'no-config',
'data-testid': 'no-config',
})
expect(wrapper.exists()).toBe(true)

View File

@ -44,7 +44,7 @@ export class ConfigFieldHandler extends PureComponent<Props> {
const {configFields, telegrafPlugin, onSetConfigArrayValue} = this.props
if (!configFields) {
return <p data-test={'no-config'}>No configuration required.</p>
return <p data-testid={'no-config'}>No configuration required.</p>
}
return Object.entries(configFields).map(

View File

@ -64,7 +64,7 @@ describe('DataLoaders.Components.CollectorsWizard.Configure.PluginConfigForm', (
telegrafPlugin,
})
const link = wrapper.find({'data-test': 'docs-link'})
const link = wrapper.find({'data-testid': 'docs-link'})
expect(link.exists()).toBe(true)
expect(link.prop('href')).toContain(telegrafPlugin.name)

View File

@ -52,7 +52,7 @@ export class PluginConfigForm extends PureComponent<Props> {
For more information about this plugin, see{' '}
<a
target="_blank"
data-test="docs-link"
data-testid="docs-link"
href={`https://github.com/influxdata/telegraf/tree/master/plugins/inputs/${
telegrafPlugin.name
}`}

View File

@ -21,6 +21,7 @@ exports[`ScraperTarget rendering renders correctly with bucket 1`] = `
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Name"
type="text"
value=""
@ -54,6 +55,7 @@ exports[`ScraperTarget rendering renders correctly with bucket 1`] = `
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Target URL"
type="text"
value=""
@ -85,6 +87,7 @@ exports[`ScraperTarget rendering renders correctly with name 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Name"
type="text"
value="MyScraper"
@ -118,6 +121,7 @@ exports[`ScraperTarget rendering renders correctly with name 1`] = `
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Target URL"
type="text"
value=""
@ -149,6 +153,7 @@ exports[`ScraperTarget rendering renders correctly with no bucket, url, name 1`]
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Name"
type="text"
value=""
@ -182,6 +187,7 @@ exports[`ScraperTarget rendering renders correctly with no bucket, url, name 1`]
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Target URL"
type="text"
value=""
@ -213,6 +219,7 @@ exports[`ScraperTarget rendering renders correctly with url 1`] = `
size="md"
spellCheck={false}
status="error"
testID="input-field"
titleText="Name"
type="text"
value=""
@ -246,6 +253,7 @@ exports[`ScraperTarget rendering renders correctly with url 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Target URL"
type="text"
value="http://url.com"

View File

@ -29,7 +29,7 @@ describe('Account', () => {
it('displays the users info by default', () => {
const {wrapper} = setup()
const nameInput = wrapper.find({'data-test': 'nameInput'})
const nameInput = wrapper.find({'data-testid': 'nameInput'})
expect(nameInput.props().value).toBe(me.name)
})
})

View File

@ -44,7 +44,7 @@ export class Settings extends PureComponent<StateProps, State> {
<Form.Element label="Username">
<Input
value={me.name}
dataTest="nameInput"
testID="nameInput"
titleText="Username"
size={ComponentSize.Small}
status={ComponentStatus.Disabled}

View File

@ -28,7 +28,7 @@ export default class TokenRow extends PureComponent<Props> {
<a
href="#"
onClick={this.handleClickDescription}
data-test={`token-description-${id}`}
data-testid={`token-description-${id}`}
>
{description}
</a>

View File

@ -56,7 +56,7 @@ describe('Account', () => {
describe('clicking the token description', () => {
it('opens the ViewTokenModal', () => {
const description = wrapper.find({
'data-test': `token-description-${1}`,
'data-testid': `token-description-${1}`,
})
description.simulate('click')
wrapper.update()

View File

@ -111,7 +111,6 @@ exports[`Account rendering renders! 1`] = `
<Input
autoFocus={false}
autocomplete="off"
dataTest="nameInput"
disabledTitleText="This input is disabled"
name=""
onChange={[Function]}
@ -119,7 +118,9 @@ exports[`Account rendering renders! 1`] = `
size="sm"
spellCheck={false}
status="disabled"
testID="nameInput"
titleText="Username"
type="text"
value="groot"
>
<div
@ -134,13 +135,14 @@ exports[`Account rendering renders! 1`] = `
autoComplete="off"
autoFocus={false}
className="input-field"
data-test="nameInput"
data-testid="nameInput"
disabled={true}
name=""
onChange={[Function]}
placeholder=""
spellCheck={false}
title="This input is disabled"
type="text"
value="groot"
/>
<div

View File

@ -19,7 +19,9 @@ exports[`Account rendering renders! 1`] = `
size="sm"
spellCheck={false}
status="default"
testID="input-field"
titleText=""
type="text"
value=""
widthPixels={256}
>
@ -35,12 +37,14 @@ exports[`Account rendering renders! 1`] = `
autoComplete="off"
autoFocus={false}
className="input-field"
data-testid="input-field"
disabled={false}
name=""
onChange={[Function]}
placeholder="Filter Tokens..."
spellCheck={false}
title=""
type="text"
value=""
/>
<span
@ -244,6 +248,7 @@ exports[`Account rendering renders! 1`] = `
emptyState={
<EmptyState
size="lg"
testID="empty-state"
>
<EmptyStateText
text="There are not any Tokens associated with this account. Contact your administrator"
@ -306,7 +311,7 @@ exports[`Account rendering renders! 1`] = `
className="index-list--cell"
>
<a
data-test="token-description-1"
data-testid="token-description-1"
href="#"
onClick={[Function]}
>
@ -426,7 +431,7 @@ exports[`Account rendering renders! 1`] = `
className="index-list--cell"
>
<a
data-test="token-description-2"
data-testid="token-description-2"
href="#"
onClick={[Function]}
>
@ -506,7 +511,7 @@ exports[`Account rendering renders! 1`] = `
>
<div
className="overlay--dialog"
data-test="overlay-children"
data-testid="overlay-children"
/>
<div
className="overlay--mask"

View File

@ -32,8 +32,8 @@ describe('Onboarding.Components.OnboardingButtons', () => {
onClickBack,
})
const nextButton = wrapper.find('[data-test="next"]')
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-testid="next"]')
const backButton = wrapper.find('[data-testid="back"]')
backButton.simulate('click')
@ -53,7 +53,7 @@ describe('Onboarding.Components.OnboardingButtons', () => {
onClickSkip,
})
const skipButton = wrapper.find('[data-test="skip"]')
const skipButton = wrapper.find('[data-testid="skip"]')
skipButton.simulate('click')
expect(skipButton.exists()).toBe(true)

View File

@ -61,7 +61,7 @@ class OnboardingButtons extends PureComponent<Props> {
text={nextButtonText}
size={ComponentSize.Medium}
type={ButtonType.Submit}
data-test="next"
data-testid="next"
ref={this.submitRef}
status={nextButtonStatus}
tabIndex={0}
@ -89,7 +89,7 @@ class OnboardingButtons extends PureComponent<Props> {
text={backButtonText}
size={ComponentSize.Medium}
onClick={onClickBack}
data-test="back"
data-testid="back"
tabIndex={1}
/>
)
@ -109,7 +109,7 @@ class OnboardingButtons extends PureComponent<Props> {
color={ComponentColor.Default}
text={skipButtonText}
onClick={onClickSkip}
data-test="skip"
data-testid="skip"
/>
</div>
)

View File

@ -52,7 +52,9 @@ exports[`Onboarding.Components.AdminStep renders 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Username"
type="text"
value=""
/>
</FormElement>
@ -76,6 +78,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Password"
type="password"
value=""
@ -101,6 +104,7 @@ exports[`Onboarding.Components.AdminStep renders 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Confirm Password"
type="password"
value=""
@ -127,7 +131,9 @@ exports[`Onboarding.Components.AdminStep renders 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Initial Organization Name"
type="text"
value=""
/>
</FormElement>
@ -152,7 +158,9 @@ exports[`Onboarding.Components.AdminStep renders 1`] = `
size="md"
spellCheck={false}
status="default"
testID="input-field"
titleText="Initial Bucket Name"
type="text"
value=""
/>
</FormElement>

View File

@ -13,6 +13,7 @@ import {
ButtonShape,
ComponentSize,
ComponentColor,
IconFont,
} from '@influxdata/clockface'
import {Bucket} from '@influxdata/influx'
import {DataLoaderType} from 'src/types/v2/dataLoaders'
@ -57,6 +58,7 @@ export default class BucketRow extends PureComponent<Props> {
<IndexList.Cell alignment={Alignment.Right}>
<Context align={Alignment.Center}>
<Context.Menu
icon={IconFont.Plus}
text="Add Data"
shape={ButtonShape.Default}
color={ComponentColor.Primary}

View File

@ -64,6 +64,7 @@ export default class CreateOrgOverlay extends PureComponent<Props, State> {
value={org.name}
onChange={this.handleChangeInput}
status={nameInputStatus}
testID="create-org-name-input"
/>
</Form.Element>
</OverlayBody>
@ -74,6 +75,7 @@ export default class CreateOrgOverlay extends PureComponent<Props, State> {
type={ButtonType.Submit}
color={ComponentColor.Primary}
status={this.submitButtonStatus}
testID="create-org-submit-button"
/>
</OverlayFooter>
</Form>

View File

@ -95,7 +95,6 @@ Object {
>
<div
class="confirmation-button--tooltip-body"
data-test="confirmation-button--click-target"
data-testid="confirmation-button"
>
Confirm
@ -118,11 +117,14 @@ Object {
>
<button
class="button button-xs button-primary context-menu--toggle context-menu--primary"
data-testid="button"
data-testid="context-menu"
tabindex="0"
title="Add Data"
type="button"
>
<span
class="button-icon icon plus"
/>
Add Data
</button>
<div
@ -133,6 +135,7 @@ Object {
>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Configure Telegraf Agent
<div
@ -143,6 +146,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Line Protocol
<div
@ -153,6 +157,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Scrape Metrics
<div
@ -228,7 +233,6 @@ Object {
>
<div
class="confirmation-button--tooltip-body"
data-test="confirmation-button--click-target"
data-testid="confirmation-button"
>
Confirm
@ -251,11 +255,14 @@ Object {
>
<button
class="button button-xs button-primary context-menu--toggle context-menu--primary"
data-testid="button"
data-testid="context-menu"
tabindex="0"
title="Add Data"
type="button"
>
<span
class="button-icon icon plus"
/>
Add Data
</button>
<div
@ -266,6 +273,7 @@ Object {
>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Configure Telegraf Agent
<div
@ -276,6 +284,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Line Protocol
<div
@ -286,6 +295,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Scrape Metrics
<div
@ -308,7 +318,7 @@ Object {
>
<div
class="overlay--dialog"
data-test="overlay-children"
data-testid="overlay-children"
/>
<div
class="overlay--mask"
@ -319,7 +329,7 @@ Object {
>
<div
class="overlay--dialog"
data-test="overlay-children"
data-testid="overlay-children"
/>
<div
class="overlay--mask"
@ -418,7 +428,6 @@ Object {
>
<div
class="confirmation-button--tooltip-body"
data-test="confirmation-button--click-target"
data-testid="confirmation-button"
>
Confirm
@ -441,11 +450,14 @@ Object {
>
<button
class="button button-xs button-primary context-menu--toggle context-menu--primary"
data-testid="button"
data-testid="context-menu"
tabindex="0"
title="Add Data"
type="button"
>
<span
class="button-icon icon plus"
/>
Add Data
</button>
<div
@ -456,6 +468,7 @@ Object {
>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Configure Telegraf Agent
<div
@ -466,6 +479,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Line Protocol
<div
@ -476,6 +490,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Scrape Metrics
<div
@ -551,7 +566,6 @@ Object {
>
<div
class="confirmation-button--tooltip-body"
data-test="confirmation-button--click-target"
data-testid="confirmation-button"
>
Confirm
@ -574,11 +588,14 @@ Object {
>
<button
class="button button-xs button-primary context-menu--toggle context-menu--primary"
data-testid="button"
data-testid="context-menu"
tabindex="0"
title="Add Data"
type="button"
>
<span
class="button-icon icon plus"
/>
Add Data
</button>
<div
@ -589,6 +606,7 @@ Object {
>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Configure Telegraf Agent
<div
@ -599,6 +617,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Line Protocol
<div
@ -609,6 +628,7 @@ Object {
</button>
<button
class="context-menu--item"
data-testid="context-menu-item"
>
Scrape Metrics
<div
@ -631,7 +651,7 @@ Object {
>
<div
class="overlay--dialog"
data-test="overlay-children"
data-testid="overlay-children"
/>
<div
class="overlay--mask"
@ -642,7 +662,7 @@ Object {
>
<div
class="overlay--dialog"
data-test="overlay-children"
data-testid="overlay-children"
/>
<div
class="overlay--mask"

View File

@ -73,6 +73,7 @@ class OrganizationsIndex extends PureComponent<Props, State> {
icon={IconFont.Plus}
text="Create Organization"
titleText="Create a new Organization"
testID="create-org-button"
/>
</Page.Header.Right>
</Page.Header>

View File

@ -73,7 +73,7 @@ const DropdownMenu: SFC<Props> = ({
[menuClass]: menuClass,
})}
style={{width: menuWidth}}
data-test="dropdown-ul"
data-testid="dropdown-ul"
>
<FancyScrollbar
autoHide={false}

View File

@ -52,7 +52,7 @@ const DropdownMenuItem: SFC<ItemProps> = ({
highlight: index === highlightedItemIndex,
active: item.text === selected,
})}
data-test="dropdown-item"
data-testid="dropdown-item"
>
<a href="#" onClick={onSelection(item)} onMouseOver={onHighlight(index)}>
{item.text}

View File

@ -37,7 +37,7 @@ describe('Components.Shared.InputClickToEdit', () => {
const inputField = inputClickToEdit.children().find('input')
const disabledDiv = inputClickToEdit
.children()
.find({'data-test': 'disabled'})
.find({'data-testid': 'disabled'})
expect(initialDiv.exists()).toBe(true)
expect(inputField.exists()).toBe(false)
@ -51,7 +51,7 @@ describe('Components.Shared.InputClickToEdit', () => {
const inputField = inputClickToEdit.children().find('input')
const disabledDiv = inputClickToEdit
.children()
.find({'data-test': 'disabled'})
.find({'data-testid': 'disabled'})
expect(inputField.exists()).toBe(false)
expect(disabledDiv.exists()).toBe(true)
@ -62,9 +62,9 @@ describe('Components.Shared.InputClickToEdit', () => {
const {inputClickToEdit} = setup({disabled})
const disabledDiv = inputClickToEdit
.children()
.find({'data-test': 'disabled'})
.find({'data-testid': 'disabled'})
const icon = disabledDiv.children().find({
'data-test': 'icon',
'data-testid': 'icon',
})
expect(icon.exists()).toBe(false)

View File

@ -116,7 +116,7 @@ class InputClickToEdit extends PureComponent<Props, State> {
return disabled ? (
<div className={wrapperClass}>
<div data-test="disabled" className="input-cte__disabled">
<div data-testid="disabled" className="input-cte__disabled">
{value}
</div>
</div>
@ -144,7 +144,7 @@ class InputClickToEdit extends PureComponent<Props, State> {
>
<span className="input-cte-span">{value || placeholder}</span>
{appearAsNormalInput || (
<span data-test="icon" className="icon pencil" />
<span data-testid="icon" className="icon pencil" />
)}
</div>
)}

View File

@ -67,14 +67,14 @@ describe('SubSections', () => {
describe('render', () => {
it('renders the currently active tab', () => {
const wrapper = setup()
const content = wrapper.dive().find({'data-test': 'subsectionContent'})
const content = wrapper.dive().find({'data-testid': 'subsectionContent'})
expect(content.find(Guava).exists()).toBe(true)
})
it('only renders enabled tabs', () => {
const wrapper = setup()
const nav = wrapper.dive().find({'data-test': 'subsectionNav'})
const nav = wrapper.dive().find({'data-testid': 'subsectionNav'})
const tabs = nav.find(SubSectionsTab)

View File

@ -25,7 +25,7 @@ class SubSections extends Component<Props> {
return (
<div className="row subsection">
<div className="col-md-2 subsection--nav" data-test="subsectionNav">
<div className="col-md-2 subsection--nav" data-testid="subsectionNav">
<div className="subsection--tabs">
{sections.map(
section =>
@ -42,7 +42,7 @@ class SubSections extends Component<Props> {
</div>
<div
className="col-md-10 subsection--content"
data-test="subsectionContent"
data-testid="subsectionContent"
>
{this.activeSectionComponent}
</div>

View File

@ -65,7 +65,7 @@ class Notification extends Component<Props, State> {
<div
className={this.notificationClassname}
ref={this.handleNotificationRef}
data-testid={this.dataTest}
data-testid={this.dataTestID}
>
<span className={`icon ${icon}`} />
<div className="notification-message">{message}</div>
@ -75,7 +75,7 @@ class Notification extends Component<Props, State> {
)
}
private get dataTest(): string {
private get dataTestID(): string {
const {type} = this.props.notification
return `notification-${type}`
}

View File

@ -20,7 +20,9 @@ exports[`TaskForm rendering renders 1`] = `
size="sm"
spellCheck={false}
status="default"
testID="input-field"
titleText=""
type="text"
value=""
/>
</FormElement>

View File

@ -106,7 +106,7 @@ class TimeFormat extends PureComponent<Props, State> {
spellCheck={false}
placeholder="Enter custom format..."
value={format}
data-test="custom-time-format"
data-testid="custom-time-format"
customClass="custom-time-format"
onChange={this.handleChangeFormat}
/>