Various Polish Stories (#2377)

* Add helpful tooltip to download CSV button

* Expose settings and tokens in navbar

* Remove gap around auto refresh dropdown

* Refactor Empty state styles

* Standardize tasks and dashboards empty states

* Standardize resource empty states in home page

* Refactor dashboard empty state

* Amend

* Standardize name and appearance of new dashboards and cells

* Ensure pause button doesn't press up against dropdown
pull/10616/head
alexpaxton 2019-01-09 13:55:49 -08:00 committed by GitHub
parent 28ebc88b44
commit c5099e7be8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 219 additions and 94 deletions

View File

@ -3,6 +3,8 @@
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
*/ */
@import 'src/style/modules';
.empty-state { .empty-state {
display: flex; display: flex;
width: 100%; width: 100%;
@ -12,42 +14,59 @@
.empty-state--text, .empty-state--text,
.empty-state--sub-text { .empty-state--sub-text {
margin-bottom: 0;
text-align: center;
color: $empty-state-text; color: $empty-state-text;
@extend %no-user-select; @extend %no-user-select;
em {
font-weight: 600;
font-style: normal;
color: $empty-state-highlight;
}
}
/*
Size Modifiers
------------------------------------------------------------------------------
*/
@mixin emptyStateSizeModifier($fontSize, $padding) {
padding: ceil($padding * 2.75) 0;
.empty-state--text,
.empty-state--sub-text {
margin-top: ceil($fontSize * 0.75);
}
.button {
margin-top: $padding;
}
> *:last-child {
margin-bottom: ceil($fontSize * 0.75);
}
.empty-state--text {
font-size: ceil($fontSize * 1.25);
}
.empty-state--sub-text {
font-size: $fontSize;
}
} }
.empty-state--xs { .empty-state--xs {
padding: $ix-marg-a 0; @include emptyStateSizeModifier($form-xs-font, $ix-marg-a);
.empty-state--text,
.empty-state--sub-text {
margin: $ix-marg-a 0;
}
} }
.empty-state--sm { .empty-state--sm {
padding: $ix-marg-c 0; @include emptyStateSizeModifier($form-sm-font, $ix-marg-b);
.empty-state--text,
.empty-state--sub-text {
margin: $ix-marg-b 0;
}
} }
.empty-state--md { .empty-state--md {
padding: $ix-marg-d 0; @include emptyStateSizeModifier($form-md-font, $ix-marg-c);
.empty-state--text,
.empty-state--sub-text {
margin: $ix-marg-b 0;
}
} }
.empty-state--lg { .empty-state--lg {
padding: $ix-marg-f 0; @include emptyStateSizeModifier($form-lg-font, $ix-marg-d);
.empty-state--text,
.empty-state--sub-text {
margin: $ix-marg-c 0;
}
} }

View File

@ -8,6 +8,10 @@ import EmptyStateSubText from 'src/clockface/components/empty_state/EmptyStateSu
// Types // Types
import {ComponentSize} from 'src/clockface/types' import {ComponentSize} from 'src/clockface/types'
// Styles
import 'src/clockface/components/empty_state/EmptyState.scss'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {

View File

@ -1,12 +1,38 @@
// Libraries // Libraries
import React, {SFC} from 'react' import React, {SFC, Fragment} from 'react'
import _ from 'lodash'
import uuid from 'uuid'
interface Props { interface Props {
text: string text: string
highlightWords?: string | string[]
} }
const EmptyStateText: SFC<Props> = ({text}) => ( const highlighter = (
<h4 className="empty-state--text">{text}</h4> text: string,
highlightWords: string | string[]
): JSX.Element[] => {
const splitString = text.replace(/[\\][n]/g, 'LINEBREAK').split(' ')
return splitString.map(word => {
if (_.includes(highlightWords, word)) {
return <em key={uuid.v4()}>{`${word}`}</em>
}
if (word === 'LINEBREAK') {
return <br />
}
if (word === 'SPACECHAR') {
return <Fragment key={uuid.v4()}>&nbsp;</Fragment>
}
return <Fragment key={uuid.v4()}>{`${word} `}</Fragment>
})
}
const EmptyStateText: SFC<Props> = ({text, highlightWords}) => (
<h4 className="empty-state--text">{highlighter(text, highlightWords)}</h4>
) )
export default EmptyStateText export default EmptyStateText

View File

@ -23,6 +23,7 @@ interface Props {
onPositionChange: (cells: Cell[]) => void onPositionChange: (cells: Cell[]) => void
setScrollTop: (e: MouseEvent<HTMLElement>) => void setScrollTop: (e: MouseEvent<HTMLElement>) => void
onEditView: (viewID: string) => void onEditView: (viewID: string) => void
onAddCell: () => void
} }
@ErrorHandling @ErrorHandling
@ -40,6 +41,7 @@ class DashboardComponent extends PureComponent<Props> {
onPositionChange, onPositionChange,
inPresentationMode, inPresentationMode,
setScrollTop, setScrollTop,
onAddCell,
} = this.props } = this.props
return ( return (
@ -63,7 +65,7 @@ class DashboardComponent extends PureComponent<Props> {
onEditView={onEditView} onEditView={onEditView}
/> />
) : ( ) : (
<DashboardEmpty dashboard={dashboard} /> <DashboardEmpty onAddCell={onAddCell} />
)} )}
{/* This element is used as a portal container for note tooltips in cell headers */} {/* This element is used as a portal container for note tooltips in cell headers */}
<div className="cell-header-note-tooltip-container" /> <div className="cell-header-note-tooltip-container" />

View File

@ -10,6 +10,12 @@ import GraphTips from 'src/shared/components/graph_tips/GraphTips'
import RenamablePageTitle from 'src/pageLayout/components/RenamablePageTitle' import RenamablePageTitle from 'src/pageLayout/components/RenamablePageTitle'
import {Button, ButtonShape, ComponentColor, IconFont} from 'src/clockface' import {Button, ButtonShape, ComponentColor, IconFont} from 'src/clockface'
// Constants
import {
DEFAULT_DASHBOARD_NAME,
DASHBOARD_NAME_MAX_LENGTH,
} from 'src/dashboards/constants/index'
// Actions // Actions
import {addNote} from 'src/dashboards/actions/v2/notes' import {addNote} from 'src/dashboards/actions/v2/notes'
@ -122,8 +128,10 @@ class DashboardHeader extends Component<Props> {
if (dashboard) { if (dashboard) {
return ( return (
<RenamablePageTitle <RenamablePageTitle
maxLength={DASHBOARD_NAME_MAX_LENGTH}
onRename={onRenameDashboard} onRename={onRenameDashboard}
name={activeDashboard} name={activeDashboard}
placeholder={DEFAULT_DASHBOARD_NAME}
/> />
) )
} }

View File

@ -213,6 +213,7 @@ class DashboardPage extends Component<Props, State> {
onPositionChange={this.handlePositionChange} onPositionChange={this.handlePositionChange}
onDeleteCell={this.handleDeleteDashboardCell} onDeleteCell={this.handleDeleteDashboardCell}
onEditView={this.handleEditView} onEditView={this.handleEditView}
onAddCell={this.handleAddCell}
/> />
)} )}
<OverlayTechnology visible={isShowingVEO}> <OverlayTechnology visible={isShowingVEO}>

View File

@ -12,6 +12,12 @@ import {
} from 'src/clockface' } from 'src/clockface'
import {Page} from 'src/pageLayout' import {Page} from 'src/pageLayout'
// Constants
import {
DEFAULT_CELL_NAME,
CELL_NAME_MAX_LENGTH,
} from 'src/dashboards/constants/index'
interface Props { interface Props {
name: string name: string
onSetName: (name: string) => void onSetName: (name: string) => void
@ -27,7 +33,12 @@ class VEOHeader extends PureComponent<Props> {
<div className="veo-header"> <div className="veo-header">
<Page.Header fullWidth={true}> <Page.Header fullWidth={true}>
<Page.Header.Left> <Page.Header.Left>
<RenamablePageTitle name={name} onRename={onSetName} /> <RenamablePageTitle
name={name}
onRename={onSetName}
placeholder={DEFAULT_CELL_NAME}
maxLength={CELL_NAME_MAX_LENGTH}
/>
</Page.Header.Left> </Page.Header.Left>
<Page.Header.Right> <Page.Header.Right>
<Button <Button

View File

@ -1,51 +1,45 @@
// Libraries // Libraries
import React, {Component} from 'react' import React, {Component} from 'react'
import {connect} from 'react-redux'
// Actions // Components
import {addCellAsync} from 'src/dashboards/actions/v2' import {
Button,
// Types IconFont,
import {Dashboard} from 'src/api' ComponentColor,
EmptyState,
ComponentSize,
} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
dashboard: Dashboard onAddCell: () => void
}
interface Actions {
addDashboardCell: typeof addCellAsync
} }
@ErrorHandling @ErrorHandling
class DashboardEmpty extends Component<Props & Actions> { class DashboardEmpty extends Component<Props> {
constructor(props) {
super(props)
}
public handleAddCell = __ => async () => {
const {dashboard, addDashboardCell} = this.props
await addDashboardCell(dashboard)
}
public render() { public render() {
const {onAddCell} = this.props
return ( return (
<div className="dashboard-empty"> <div className="dashboard-empty">
<p> <EmptyState size={ComponentSize.Large}>
This Dashboard doesn't have any <strong>Cells</strong>, why not add <EmptyState.Text
one? text="This Dashboard doesn't have any Cells , why not add
</p> one?"
highlightWords={['Cells']}
/>
<Button
text="Add Cell"
size={ComponentSize.Medium}
icon={IconFont.AddCell}
color={ComponentColor.Primary}
onClick={onAddCell}
/>
</EmptyState>
</div> </div>
) )
} }
} }
const mdtp = { export default DashboardEmpty
addDashboardCell: addCellAsync,
}
export default connect(
null,
mdtp
)(DashboardEmpty)

View File

@ -40,6 +40,9 @@ import {
dashboardCreateFailed, dashboardCreateFailed,
} from 'src/shared/copy/notifications' } from 'src/shared/copy/notifications'
// Constants
import {DEFAULT_DASHBOARD_NAME} from 'src/dashboards/constants/index'
// Types // Types
import {Notification} from 'src/types/notifications' import {Notification} from 'src/types/notifications'
import {DashboardFile} from 'src/types/v2/dashboards' import {DashboardFile} from 'src/types/v2/dashboards'
@ -152,7 +155,7 @@ class DashboardIndex extends PureComponent<Props, State> {
const {router, notify} = this.props const {router, notify} = this.props
try { try {
const newDashboard = { const newDashboard = {
name: 'Name this dashboard', name: DEFAULT_DASHBOARD_NAME,
cells: [], cells: [],
} }
const data = await createDashboard(newDashboard) const data = await createDashboard(newDashboard)
@ -215,7 +218,7 @@ class DashboardIndex extends PureComponent<Props, State> {
h: 4, h: 4,
} }
const name = _.get(dashboard, 'name', 'Name this dashboard') const name = _.get(dashboard, 'name', DEFAULT_DASHBOARD_NAME)
const cellsWithDefaultsApplied = getDeep<Cell[]>( const cellsWithDefaultsApplied = getDeep<Cell[]>(
dashboard, dashboard,
'cells', 'cells',

View File

@ -141,19 +141,23 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
if (searchTerm) { if (searchTerm) {
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Large}>
<EmptyState.Text text="No dashboards match your search term" /> <EmptyState.Text text="No Dashboards match your search term" />
</EmptyState> </EmptyState>
) )
} }
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Large}>
<EmptyState.Text text="Looks like you dont have any dashboards" /> <EmptyState.Text
text="Looks like you dont have any Dashboards , why not create one?"
highlightWords={['Dashboards']}
/>
<Button <Button
text="Create a Dashboard" text="Create a Dashboard"
icon={IconFont.Plus} icon={IconFont.Plus}
color={ComponentColor.Primary} color={ComponentColor.Primary}
onClick={onCreateDashboard} onClick={onCreateDashboard}
size={ComponentSize.Medium}
/> />
</EmptyState> </EmptyState>
) )

View File

@ -86,6 +86,7 @@ type NewDefaultDashboard = Pick<
cells: NewDefaultCell[] cells: NewDefaultCell[]
} }
> >
export const DEFAULT_CELL_NAME = 'Name this Cell'
export const DEFAULT_DASHBOARD_NAME = 'Name this Dashboard' export const DEFAULT_DASHBOARD_NAME = 'Name this Dashboard'
export const NEW_DASHBOARD: NewDefaultDashboard = { export const NEW_DASHBOARD: NewDefaultDashboard = {
name: DEFAULT_DASHBOARD_NAME, name: DEFAULT_DASHBOARD_NAME,
@ -96,6 +97,7 @@ export const TYPE_QUERY_CONFIG: string = 'queryConfig'
export const TYPE_SHIFTED: string = 'shifted queryConfig' export const TYPE_SHIFTED: string = 'shifted queryConfig'
export const TYPE_FLUX: string = 'flux' export const TYPE_FLUX: string = 'flux'
export const DASHBOARD_NAME_MAX_LENGTH: number = 50 export const DASHBOARD_NAME_MAX_LENGTH: number = 50
export const CELL_NAME_MAX_LENGTH: number = 68
export enum CEOTabs { export enum CEOTabs {
Queries = 'Queries', Queries = 'Queries',

View File

@ -2,6 +2,9 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {Link} from 'react-router' import {Link} from 'react-router'
// Components
import {EmptyState, ComponentSize} from 'src/clockface'
// Types // Types
import {Dashboard} from 'src/types' import {Dashboard} from 'src/types'
@ -12,8 +15,13 @@ interface Props {
export default class UserDashboardList extends PureComponent<Props> { export default class UserDashboardList extends PureComponent<Props> {
public render() { public render() {
const {dashboards} = this.props const {dashboards} = this.props
if (this.isEmpty) { if (this.isEmpty) {
return <div>Looks like you dont have any dashboards</div> return (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text text="You don't have any Dashboards" />
</EmptyState>
)
} }
return ( return (

View File

@ -2,6 +2,9 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {Link} from 'react-router' import {Link} from 'react-router'
// Components
import {EmptyState, ComponentSize} from 'src/clockface'
// Types // Types
import {Organization} from 'src/types/v2' import {Organization} from 'src/types/v2'
@ -14,7 +17,11 @@ export default class UserDashboardList extends PureComponent<Props> {
const {orgs} = this.props const {orgs} = this.props
if (this.isEmpty) { if (this.isEmpty) {
return <div>Looks like you dont belong to any organizations</div> return (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text text="You don't belong to any Organizations" />
</EmptyState>
)
} }
return ( return (

View File

@ -6,12 +6,6 @@ import classnames from 'classnames'
import {Input} from 'src/clockface' import {Input} from 'src/clockface'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
// Constants
import {
DASHBOARD_NAME_MAX_LENGTH,
DEFAULT_DASHBOARD_NAME,
} from 'src/dashboards/constants/index'
// Decorators // Decorators
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
@ -21,6 +15,8 @@ import 'src/pageLayout/components/RenamablePageTitle.scss'
interface Props { interface Props {
onRename: (name: string) => void onRename: (name: string) => void
name: string name: string
placeholder: string
maxLength: number
} }
interface State { interface State {
@ -41,7 +37,7 @@ class RenamablePageTitle extends Component<Props, State> {
public render() { public render() {
const {isEditing} = this.state const {isEditing} = this.state
const {name} = this.props const {name, placeholder} = this.props
if (isEditing) { if (isEditing) {
return ( return (
@ -56,7 +52,7 @@ class RenamablePageTitle extends Component<Props, State> {
return ( return (
<div className="renamable-page-title"> <div className="renamable-page-title">
<div className={this.titleClassName} onClick={this.handleStartEditing}> <div className={this.titleClassName} onClick={this.handleStartEditing}>
{name || DEFAULT_DASHBOARD_NAME} {name || placeholder}
<span className="icon pencil" /> <span className="icon pencil" />
</div> </div>
</div> </div>
@ -64,14 +60,15 @@ class RenamablePageTitle extends Component<Props, State> {
} }
private get input(): JSX.Element { private get input(): JSX.Element {
const {placeholder, maxLength} = this.props
const {workingName} = this.state const {workingName} = this.state
return ( return (
<Input <Input
maxLength={DASHBOARD_NAME_MAX_LENGTH} maxLength={maxLength}
autoFocus={true} autoFocus={true}
spellCheck={false} spellCheck={false}
placeholder={DEFAULT_DASHBOARD_NAME} placeholder={placeholder}
onFocus={this.handleInputFocus} onFocus={this.handleInputFocus}
onChange={this.handleInputChange} onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@ -119,9 +116,9 @@ class RenamablePageTitle extends Component<Props, State> {
} }
private get titleClassName(): string { private get titleClassName(): string {
const {name} = this.props const {name, placeholder} = this.props
const nameIsUntitled = name === DEFAULT_DASHBOARD_NAME || name === '' const nameIsUntitled = name === placeholder || name === ''
return classnames('renamable-page-title--title', { return classnames('renamable-page-title--title', {
untitled: nameIsUntitled, untitled: nameIsUntitled,

View File

@ -45,6 +45,18 @@ class SideNav extends PureComponent<Props> {
location={location.pathname} location={location.pathname}
highlightWhen={['me', 'account']} highlightWhen={['me', 'account']}
> >
<NavMenu.SubItem
title="Settings"
link="/account/settings"
location={location.pathname}
highlightWhen={['settings']}
/>
<NavMenu.SubItem
title="Tokens"
link="/account/tokens"
location={location.pathname}
highlightWhen={['tokens']}
/>
<NavMenu.SubItem <NavMenu.SubItem
title="Logout" title="Logout"
link="/logout" link="/logout"

View File

@ -14,18 +14,37 @@ interface Props {
class CSVExportButton extends PureComponent<Props, {}> { class CSVExportButton extends PureComponent<Props, {}> {
public render() { public render() {
const {files} = this.props
return ( return (
<Button <Button
titleText={this.titleText}
text="CSV" text="CSV"
icon={IconFont.Download} icon={IconFont.Download}
onClick={this.handleClick} onClick={this.handleClick}
status={files ? ComponentStatus.Default : ComponentStatus.Disabled} status={this.buttonStatus}
/> />
) )
} }
private get buttonStatus(): ComponentStatus {
const {files} = this.props
if (files) {
return ComponentStatus.Default
}
return ComponentStatus.Disabled
}
private get titleText(): string {
const {files} = this.props
if (files) {
return 'Download query results as a .CSV file'
}
return 'Create a query in order to download results as .CSV'
}
private handleClick = () => { private handleClick = () => {
const {files} = this.props const {files} = this.props
const csv = files.join('\n\n') const csv = files.join('\n\n')

View File

@ -3,15 +3,17 @@
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
*/ */
@import 'src/style/modules';
.autorefresh-dropdown { .autorefresh-dropdown {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
.dropdown {
margin-right: $ix-marg-a;
}
&.paused .dropdown--selected { &.paused .dropdown--selected {
opacity: 0; opacity: 0;
} }
} }
.autorefresh-dropdown--pause {
margin-left: $ix-marg-a;
}

View File

@ -11,6 +11,9 @@ import autoRefreshOptions, {
AutoRefreshOptionType, AutoRefreshOptionType,
} from 'src/shared/data/autoRefreshes' } from 'src/shared/data/autoRefreshes'
// Styles
import 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown.scss'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
@ -123,6 +126,7 @@ class AutoRefreshDropdown extends Component<Props> {
shape={ButtonShape.Square} shape={ButtonShape.Square}
icon={IconFont.Refresh} icon={IconFont.Refresh}
onClick={onManualRefresh} onClick={onManualRefresh}
customClass="autorefresh-dropdown--pause"
/> />
) )
} }

View File

@ -17,10 +17,11 @@ import {
DEFAULT_GAUGE_COLORS, DEFAULT_GAUGE_COLORS,
DEFAULT_THRESHOLDS_LIST_COLORS, DEFAULT_THRESHOLDS_LIST_COLORS,
} from 'src/shared/constants/thresholds' } from 'src/shared/constants/thresholds'
import {DEFAULT_CELL_NAME} from 'src/dashboards/constants/index'
function defaultView() { function defaultView() {
return { return {
name: 'Untitled', name: DEFAULT_CELL_NAME,
} }
} }

View File

@ -104,6 +104,7 @@ $form-lg-font: 17px;
/* Empty State */ /* Empty State */
$empty-state-text: $g9-mountain; $empty-state-text: $g9-mountain;
$empty-state-highlight: $g13-mist;
$default-font: 'Roboto', Helvetica, sans-serif; $default-font: 'Roboto', Helvetica, sans-serif;
$code-font: 'RobotoMono', monospace; $code-font: 'RobotoMono', monospace;

View File

@ -16,7 +16,6 @@
// Components // Components
// TODO: Import these styles into their respective components instead of this stylesheet // TODO: Import these styles into their respective components instead of this stylesheet
@import 'src/shared/components/ColorDropdown'; @import 'src/shared/components/ColorDropdown';
@import 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown';
@import 'src/shared/components/avatar/Avatar'; @import 'src/shared/components/avatar/Avatar';
@import 'src/shared/components/tables/TableGraphs'; @import 'src/shared/components/tables/TableGraphs';
@import 'src/shared/components/fancy_scrollbar/FancyScrollbar'; @import 'src/shared/components/fancy_scrollbar/FancyScrollbar';

View File

@ -34,10 +34,11 @@ export default class EmptyTasksLists extends PureComponent<Props> {
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Large}>
<EmptyState.Text <EmptyState.Text
text={"Looks like you don't have any Tasks, why not create one?"} text={"Looks like you don't have any Tasks , why not create one?"}
highlightWords={['Tasks']}
/> />
<Button <Button
size={ComponentSize.Small} size={ComponentSize.Medium}
color={ComponentColor.Primary} color={ComponentColor.Primary}
icon={IconFont.Plus} icon={IconFont.Plus}
text="Create Task" text="Create Task"
@ -49,7 +50,7 @@ export default class EmptyTasksLists extends PureComponent<Props> {
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Large}>
<EmptyState.Text text={'No tasks match your search term'} /> <EmptyState.Text text={'No Tasks match your search term'} />
</EmptyState> </EmptyState>
) )
} }