More Polish (#2394)
* Remove Import/Export buttons from dashboards index * Reduce max label chars to 50 * Change format of dashboard "modified" column to relative time Absolute time is included as a tooltip * Redistribute column widths in dashboards table Optimzied for long names * Improve component spacer Now supports more fine grained "stretch to fit" controls * Introduce editable description component Intended mainly for use in index list views * Allow dashboard descriptions to be editable in place * Give modified column a tad more space * Standardize empty states of tabs in organization view * Update testpull/10616/head
parent
7875051ace
commit
7cf643d1d5
|
@ -6,6 +6,14 @@
|
||||||
.component-spacer {
|
.component-spacer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
&.component-spacer--stretch-w {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.component-spacer--stretch-h {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-spacer--left {
|
.component-spacer--left {
|
||||||
|
@ -24,10 +32,6 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&.component-spacer--stretch {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.component-spacer--left > * {
|
&.component-spacer--left > * {
|
||||||
margin-right: $ix-marg-a;
|
margin-right: $ix-marg-a;
|
||||||
|
|
||||||
|
@ -53,10 +57,6 @@
|
||||||
.component-spacer--vertical {
|
.component-spacer--vertical {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.component-spacer--stretch {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.component-spacer--left {
|
&.component-spacer--left {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,16 @@ interface Props {
|
||||||
children: JSX.Element | JSX.Element[]
|
children: JSX.Element | JSX.Element[]
|
||||||
align: Alignment
|
align: Alignment
|
||||||
stackChildren?: Stack
|
stackChildren?: Stack
|
||||||
stretchToFit?: boolean
|
stretchToFitWidth?: boolean
|
||||||
|
stretchToFitHeight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComponentSpacer: SFC<Props> = ({
|
const ComponentSpacer: SFC<Props> = ({
|
||||||
children,
|
children,
|
||||||
align,
|
align,
|
||||||
stackChildren = Stack.Columns,
|
stackChildren = Stack.Columns,
|
||||||
stretchToFit = false,
|
stretchToFitWidth = false,
|
||||||
|
stretchToFitHeight = false,
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={classnames('component-spacer', {
|
className={classnames('component-spacer', {
|
||||||
|
@ -25,7 +27,8 @@ const ComponentSpacer: SFC<Props> = ({
|
||||||
'component-spacer--right': align === Alignment.Right,
|
'component-spacer--right': align === Alignment.Right,
|
||||||
'component-spacer--horizontal': stackChildren === Stack.Columns,
|
'component-spacer--horizontal': stackChildren === Stack.Columns,
|
||||||
'component-spacer--vertical': stackChildren === Stack.Rows,
|
'component-spacer--vertical': stackChildren === Stack.Rows,
|
||||||
'component-spacer--stretch': stretchToFit,
|
'component-spacer--stretch-w': stretchToFitWidth,
|
||||||
|
'component-spacer--stretch-h': stretchToFitHeight,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -200,18 +200,6 @@
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.index-list--description {
|
|
||||||
user-select: none;
|
|
||||||
font-size: 12px;
|
|
||||||
color: $g12-forge;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.untitled {
|
|
||||||
color: $g9-mountain;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.index-list--labels {
|
.index-list--labels {
|
||||||
margin-left: $ix-marg-b;
|
margin-left: $ix-marg-b;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
||||||
onCloneDashboard: (dashboard: Dashboard) => void
|
onCloneDashboard: (dashboard: Dashboard) => void
|
||||||
onExportDashboard: (dashboard: Dashboard) => void
|
onExportDashboard: (dashboard: Dashboard) => void
|
||||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||||
|
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||||
notify: (message: Notification) => void
|
notify: (message: Notification) => void
|
||||||
searchTerm: string
|
searchTerm: string
|
||||||
}
|
}
|
||||||
|
@ -34,6 +35,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
||||||
onCreateDashboard,
|
onCreateDashboard,
|
||||||
defaultDashboardLink,
|
defaultDashboardLink,
|
||||||
onSetDefaultDashboard,
|
onSetDefaultDashboard,
|
||||||
|
onUpdateDashboard,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
|
@ -48,6 +50,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
||||||
onExportDashboard={onExportDashboard}
|
onExportDashboard={onExportDashboard}
|
||||||
defaultDashboardLink={defaultDashboardLink}
|
defaultDashboardLink={defaultDashboardLink}
|
||||||
onSetDefaultDashboard={onSetDefaultDashboard}
|
onSetDefaultDashboard={onSetDefaultDashboard}
|
||||||
|
onUpdateDashboard={onUpdateDashboard}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
getDashboardsAsync,
|
getDashboardsAsync,
|
||||||
importDashboardAsync,
|
importDashboardAsync,
|
||||||
deleteDashboardAsync,
|
deleteDashboardAsync,
|
||||||
|
updateDashboardAsync,
|
||||||
} from 'src/dashboards/actions/v2'
|
} from 'src/dashboards/actions/v2'
|
||||||
import {setDefaultDashboard} from 'src/shared/actions/links'
|
import {setDefaultDashboard} from 'src/shared/actions/links'
|
||||||
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
|
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
|
||||||
|
@ -59,6 +60,7 @@ interface Props {
|
||||||
handleGetDashboards: typeof getDashboardsAsync
|
handleGetDashboards: typeof getDashboardsAsync
|
||||||
handleDeleteDashboard: typeof deleteDashboardAsync
|
handleDeleteDashboard: typeof deleteDashboardAsync
|
||||||
handleImportDashboard: typeof importDashboardAsync
|
handleImportDashboard: typeof importDashboardAsync
|
||||||
|
handleUpdateDashboard: typeof updateDashboardAsync
|
||||||
notify: (message: Notification) => void
|
notify: (message: Notification) => void
|
||||||
retainRangesDashTimeV1: (dashboardIDs: string[]) => void
|
retainRangesDashTimeV1: (dashboardIDs: string[]) => void
|
||||||
dashboards: Dashboard[]
|
dashboards: Dashboard[]
|
||||||
|
@ -88,7 +90,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {dashboards, notify, links} = this.props
|
const {dashboards, notify, links, handleUpdateDashboard} = this.props
|
||||||
const {searchTerm} = this.state
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -103,12 +105,6 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
placeholderText="Filter dashboards by name..."
|
placeholderText="Filter dashboards by name..."
|
||||||
onSearch={this.filterDashboards}
|
onSearch={this.filterDashboards}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
onClick={this.handleToggleOverlay}
|
|
||||||
icon={IconFont.Import}
|
|
||||||
text="Import"
|
|
||||||
titleText="Import a dashboard from a file"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
color={ComponentColor.Primary}
|
color={ComponentColor.Primary}
|
||||||
onClick={this.handleCreateDashboard}
|
onClick={this.handleCreateDashboard}
|
||||||
|
@ -127,6 +123,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
onCreateDashboard={this.handleCreateDashboard}
|
onCreateDashboard={this.handleCreateDashboard}
|
||||||
onCloneDashboard={this.handleCloneDashboard}
|
onCloneDashboard={this.handleCloneDashboard}
|
||||||
onExportDashboard={this.handleExportDashboard}
|
onExportDashboard={this.handleExportDashboard}
|
||||||
|
onUpdateDashboard={handleUpdateDashboard}
|
||||||
notify={notify}
|
notify={notify}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
/>
|
/>
|
||||||
|
@ -271,6 +268,7 @@ const mdtp = {
|
||||||
handleGetDashboards: getDashboardsAsync,
|
handleGetDashboards: getDashboardsAsync,
|
||||||
handleDeleteDashboard: deleteDashboardAsync,
|
handleDeleteDashboard: deleteDashboardAsync,
|
||||||
handleImportDashboard: importDashboardAsync,
|
handleImportDashboard: importDashboardAsync,
|
||||||
|
handleUpdateDashboard: updateDashboardAsync,
|
||||||
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
|
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ interface Props {
|
||||||
onCreateDashboard: () => void
|
onCreateDashboard: () => void
|
||||||
onCloneDashboard: (dashboard: Dashboard) => void
|
onCloneDashboard: (dashboard: Dashboard) => void
|
||||||
onExportDashboard: (dashboard: Dashboard) => void
|
onExportDashboard: (dashboard: Dashboard) => void
|
||||||
|
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||||
onSetDefaultDashboard: (dashboardLink: string) => void
|
onSetDefaultDashboard: (dashboardLink: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,26 +63,26 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
||||||
columnName={headerKeys[0]}
|
columnName={headerKeys[0]}
|
||||||
sortKey={headerKeys[0]}
|
sortKey={headerKeys[0]}
|
||||||
sort={sortKey === headerKeys[0] ? sortDirection : Sort.None}
|
sort={sortKey === headerKeys[0] ? sortDirection : Sort.None}
|
||||||
width="50%"
|
width="62%"
|
||||||
onClick={this.handleClickColumn}
|
onClick={this.handleClickColumn}
|
||||||
/>
|
/>
|
||||||
<IndexList.HeaderCell
|
<IndexList.HeaderCell
|
||||||
columnName={headerKeys[1]}
|
columnName={headerKeys[1]}
|
||||||
sortKey={headerKeys[1]}
|
sortKey={headerKeys[1]}
|
||||||
sort={sortKey === headerKeys[1] ? sortDirection : Sort.None}
|
sort={sortKey === headerKeys[1] ? sortDirection : Sort.None}
|
||||||
width="10%"
|
width="17%"
|
||||||
onClick={this.handleClickColumn}
|
onClick={this.handleClickColumn}
|
||||||
/>
|
/>
|
||||||
<IndexList.HeaderCell
|
<IndexList.HeaderCell
|
||||||
columnName={headerKeys[2]}
|
columnName={headerKeys[2]}
|
||||||
sortKey={headerKeys[2]}
|
sortKey={headerKeys[2]}
|
||||||
sort={sortKey === headerKeys[2] ? sortDirection : Sort.None}
|
sort={sortKey === headerKeys[2] ? sortDirection : Sort.None}
|
||||||
width="30%"
|
width="11%"
|
||||||
onClick={this.handleClickColumn}
|
onClick={this.handleClickColumn}
|
||||||
/>
|
/>
|
||||||
<IndexList.HeaderCell
|
<IndexList.HeaderCell
|
||||||
columnName=""
|
columnName=""
|
||||||
width="20%"
|
width="10%"
|
||||||
alignment={Alignment.Right}
|
alignment={Alignment.Right}
|
||||||
/>
|
/>
|
||||||
</IndexList.Header>
|
</IndexList.Header>
|
||||||
|
@ -102,6 +103,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
||||||
onExportDashboard,
|
onExportDashboard,
|
||||||
onCloneDashboard,
|
onCloneDashboard,
|
||||||
onDeleteDashboard,
|
onDeleteDashboard,
|
||||||
|
onUpdateDashboard,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const {sortKey, sortDirection} = this.state
|
const {sortKey, sortDirection} = this.state
|
||||||
|
@ -119,6 +121,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
||||||
onCloneDashboard={onCloneDashboard}
|
onCloneDashboard={onCloneDashboard}
|
||||||
onExportDashboard={onExportDashboard}
|
onExportDashboard={onExportDashboard}
|
||||||
onDeleteDashboard={onDeleteDashboard}
|
onDeleteDashboard={onDeleteDashboard}
|
||||||
|
onUpdateDashboard={onUpdateDashboard}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SortingHat>
|
</SortingHat>
|
||||||
|
|
|
@ -15,6 +15,7 @@ const setup = (override = {}) => {
|
||||||
onDeleteDashboard: jest.fn(),
|
onDeleteDashboard: jest.fn(),
|
||||||
onCloneDashboard: jest.fn(),
|
onCloneDashboard: jest.fn(),
|
||||||
onExportDashboard: jest.fn(),
|
onExportDashboard: jest.fn(),
|
||||||
|
onUpdateDashboard: jest.fn(),
|
||||||
...override,
|
...override,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Label,
|
Label,
|
||||||
} from 'src/clockface'
|
} from 'src/clockface'
|
||||||
|
import EditableDescription from 'src/shared/components/editable_description/EditableDescription'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {Dashboard} from 'src/types/v2'
|
import {Dashboard} from 'src/types/v2'
|
||||||
|
@ -30,6 +31,7 @@ interface Props {
|
||||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||||
onCloneDashboard: (dashboard: Dashboard) => void
|
onCloneDashboard: (dashboard: Dashboard) => void
|
||||||
onExportDashboard: (dashboard: Dashboard) => void
|
onExportDashboard: (dashboard: Dashboard) => void
|
||||||
|
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||||
|
@ -40,7 +42,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<IndexList.Row key={`dashboard-id--${id}`} disabled={false}>
|
<IndexList.Row key={`dashboard-id--${id}`} disabled={false}>
|
||||||
<IndexList.Cell>
|
<IndexList.Cell>
|
||||||
<ComponentSpacer stackChildren={Stack.Rows} align={Alignment.Left}>
|
<ComponentSpacer
|
||||||
|
stackChildren={Stack.Rows}
|
||||||
|
align={Alignment.Left}
|
||||||
|
stretchToFitWidth={true}
|
||||||
|
>
|
||||||
<ComponentSpacer
|
<ComponentSpacer
|
||||||
stackChildren={Stack.Columns}
|
stackChildren={Stack.Columns}
|
||||||
align={Alignment.Left}
|
align={Alignment.Left}
|
||||||
|
@ -50,21 +56,17 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||||
</Link>
|
</Link>
|
||||||
{this.labels}
|
{this.labels}
|
||||||
</ComponentSpacer>
|
</ComponentSpacer>
|
||||||
{this.description}
|
<EditableDescription
|
||||||
|
description={dashboard.description}
|
||||||
|
placeholder={`Describe ${dashboard.name}`}
|
||||||
|
onUpdate={this.handleUpdateDescription}
|
||||||
|
/>
|
||||||
</ComponentSpacer>
|
</ComponentSpacer>
|
||||||
</IndexList.Cell>
|
</IndexList.Cell>
|
||||||
<IndexList.Cell>Owner does not come back from API</IndexList.Cell>
|
<IndexList.Cell>Owner does not come back from API</IndexList.Cell>
|
||||||
<IndexList.Cell>
|
{this.lastModifiedCell}
|
||||||
{moment(dashboard.meta.updatedAt).format(UPDATED_AT_TIME_FORMAT)}
|
|
||||||
</IndexList.Cell>
|
|
||||||
<IndexList.Cell alignment={Alignment.Right} revealOnHover={true}>
|
<IndexList.Cell alignment={Alignment.Right} revealOnHover={true}>
|
||||||
<ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}>
|
<ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}>
|
||||||
<Button
|
|
||||||
size={ComponentSize.ExtraSmall}
|
|
||||||
text="Export"
|
|
||||||
icon={IconFont.Export}
|
|
||||||
onClick={this.handleExport}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
size={ComponentSize.ExtraSmall}
|
size={ComponentSize.ExtraSmall}
|
||||||
text="Clone"
|
text="Clone"
|
||||||
|
@ -106,6 +108,21 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get lastModifiedCell(): JSX.Element {
|
||||||
|
const {dashboard} = this.props
|
||||||
|
|
||||||
|
const relativeTimestamp = moment(dashboard.meta.updatedAt).fromNow()
|
||||||
|
const absoluteTimestamp = moment(dashboard.meta.updatedAt).format(
|
||||||
|
UPDATED_AT_TIME_FORMAT
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IndexList.Cell>
|
||||||
|
<span title={absoluteTimestamp}>{relativeTimestamp}</span>
|
||||||
|
</IndexList.Cell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private get name(): string {
|
private get name(): string {
|
||||||
const {dashboard} = this.props
|
const {dashboard} = this.props
|
||||||
|
|
||||||
|
@ -120,23 +137,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get description(): JSX.Element {
|
private handleUpdateDescription = (description: string): void => {
|
||||||
const {dashboard} = this.props
|
const {onUpdateDashboard} = this.props
|
||||||
|
const dashboard = {...this.props.dashboard, description}
|
||||||
|
|
||||||
if (dashboard.description) {
|
onUpdateDashboard(dashboard)
|
||||||
return (
|
|
||||||
<div className="index-list--description">{dashboard.description}</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="index-list--description untitled">No description</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleExport = () => {
|
|
||||||
const {onExportDashboard, dashboard} = this.props
|
|
||||||
onExportDashboard(dashboard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClone = () => {
|
private handleClone = () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface Props {
|
||||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||||
onCloneDashboard: (dashboard: Dashboard) => void
|
onCloneDashboard: (dashboard: Dashboard) => void
|
||||||
onExportDashboard: (dashboard: Dashboard) => void
|
onExportDashboard: (dashboard: Dashboard) => void
|
||||||
|
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||||
|
@ -21,6 +22,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||||
onExportDashboard,
|
onExportDashboard,
|
||||||
onCloneDashboard,
|
onCloneDashboard,
|
||||||
onDeleteDashboard,
|
onDeleteDashboard,
|
||||||
|
onUpdateDashboard,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return dashboards.map(d => (
|
return dashboards.map(d => (
|
||||||
|
@ -30,6 +32,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||||
onExportDashboard={onExportDashboard}
|
onExportDashboard={onExportDashboard}
|
||||||
onCloneDashboard={onCloneDashboard}
|
onCloneDashboard={onCloneDashboard}
|
||||||
onDeleteDashboard={onDeleteDashboard}
|
onDeleteDashboard={onDeleteDashboard}
|
||||||
|
onUpdateDashboard={onUpdateDashboard}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,19 +159,29 @@ export default class Buckets extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get emptyState(): JSX.Element {
|
private get emptyState(): JSX.Element {
|
||||||
|
const {org} = this.props
|
||||||
const {searchTerm} = this.state
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
if (_.isEmpty(searchTerm)) {
|
if (_.isEmpty(searchTerm)) {
|
||||||
return (
|
return (
|
||||||
<EmptyState size={ComponentSize.Large}>
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
<EmptyState.Text text="Oh noes I dun see na buckets" />
|
<EmptyState.Text
|
||||||
|
text={`${org.name} does not own any Buckets , why not create one?`}
|
||||||
|
highlightWords={['Buckets']}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Create Bucket"
|
||||||
|
icon={IconFont.Plus}
|
||||||
|
color={ComponentColor.Primary}
|
||||||
|
onClick={this.handleOpenModal}
|
||||||
|
/>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyState size={ComponentSize.Large}>
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
<EmptyState.Text text="No buckets match your query" />
|
<EmptyState.Text text="No Buckets match your query" />
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {PureComponent, ChangeEvent} from 'react'
|
import React, {PureComponent, ChangeEvent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
||||||
|
@ -15,6 +16,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboards: Dashboard[]
|
dashboards: Dashboard[]
|
||||||
|
orgName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -66,9 +68,23 @@ export default class Dashboards extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get emptyState(): JSX.Element {
|
private get emptyState(): JSX.Element {
|
||||||
|
const {orgName} = this.props
|
||||||
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
|
if (_.isEmpty(searchTerm)) {
|
||||||
|
return (
|
||||||
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
|
<EmptyState.Text
|
||||||
|
text={`${orgName} does not own any Dashboards , why not create one?`}
|
||||||
|
highlightWords={['Dashboards']}
|
||||||
|
/>
|
||||||
|
</EmptyState>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyState size={ComponentSize.Large}>
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
<EmptyState.Text text="Oh noes I dun see na dashbardsss" />
|
<EmptyState.Text text="No Dashboards match your query" />
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
HEX_CODE_CHAR_LENGTH,
|
HEX_CODE_CHAR_LENGTH,
|
||||||
INPUT_ERROR_COLOR,
|
INPUT_ERROR_COLOR,
|
||||||
} from 'src/organizations/constants/LabelColors'
|
} from 'src/organizations/constants/LabelColors'
|
||||||
const MAX_LABEL_CHARS = 75
|
const MAX_LABEL_CHARS = 50
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import {validateHexCode} from 'src/organizations/utils/labels'
|
import {validateHexCode} from 'src/organizations/utils/labels'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {PureComponent, ChangeEvent} from 'react'
|
import React, {PureComponent, ChangeEvent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import {ComponentSize, EmptyState, IconFont, Input} from 'src/clockface'
|
import {ComponentSize, EmptyState, IconFont, Input} from 'src/clockface'
|
||||||
|
@ -15,6 +16,7 @@ import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
members: ResourceOwner[]
|
members: ResourceOwner[]
|
||||||
|
orgName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -60,9 +62,23 @@ export default class Members extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get emptyState(): JSX.Element {
|
private get emptyState(): JSX.Element {
|
||||||
|
const {orgName} = this.props
|
||||||
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
|
if (_.isEmpty(searchTerm)) {
|
||||||
|
return (
|
||||||
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
|
<EmptyState.Text
|
||||||
|
text={`${orgName} doesn't have any Members , why not invite some?`}
|
||||||
|
highlightWords={['Members']}
|
||||||
|
/>
|
||||||
|
</EmptyState>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyState size={ComponentSize.Medium}>
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
<EmptyState.Text text="This org has been abandoned" />
|
<EmptyState.Text text="No Members match your query" />
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {PureComponent, ChangeEvent} from 'react'
|
import React, {PureComponent, ChangeEvent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
||||||
|
@ -12,6 +13,7 @@ import {Task} from 'src/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tasks: Task[]
|
tasks: Task[]
|
||||||
|
orgName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -62,9 +64,23 @@ export default class Tasks extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get emptyState(): JSX.Element {
|
private get emptyState(): JSX.Element {
|
||||||
|
const {orgName} = this.props
|
||||||
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
|
if (_.isEmpty(searchTerm)) {
|
||||||
|
return (
|
||||||
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
|
<EmptyState.Text
|
||||||
|
text={`${orgName} does not own any Tasks , why not create one?`}
|
||||||
|
highlightWords={'Tasks'}
|
||||||
|
/>
|
||||||
|
</EmptyState>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyState size={ComponentSize.Medium}>
|
<EmptyState size={ComponentSize.Medium}>
|
||||||
<EmptyState.Text text="I see nay a task" />
|
<EmptyState.Text text="No Tasks match your query" />
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ class OrganizationView extends PureComponent<Props> {
|
||||||
>
|
>
|
||||||
{(members, loading) => (
|
{(members, loading) => (
|
||||||
<Spinner loading={loading}>
|
<Spinner loading={loading}>
|
||||||
<Members members={members} />
|
<Members members={members} orgName={org.name} />
|
||||||
</Spinner>
|
</Spinner>
|
||||||
)}
|
)}
|
||||||
</GetOrgResources>
|
</GetOrgResources>
|
||||||
|
@ -107,7 +107,7 @@ class OrganizationView extends PureComponent<Props> {
|
||||||
>
|
>
|
||||||
{(dashboards, loading) => (
|
{(dashboards, loading) => (
|
||||||
<Spinner loading={loading}>
|
<Spinner loading={loading}>
|
||||||
<Dashboards dashboards={dashboards} />
|
<Dashboards dashboards={dashboards} orgName={org.name} />
|
||||||
</Spinner>
|
</Spinner>
|
||||||
)}
|
)}
|
||||||
</GetOrgResources>
|
</GetOrgResources>
|
||||||
|
@ -120,7 +120,7 @@ class OrganizationView extends PureComponent<Props> {
|
||||||
<GetOrgResources<Task[]> organization={org} fetcher={getTasks}>
|
<GetOrgResources<Task[]> organization={org} fetcher={getTasks}>
|
||||||
{(tasks, loading) => (
|
{(tasks, loading) => (
|
||||||
<Spinner loading={loading}>
|
<Spinner loading={loading}>
|
||||||
<Tasks tasks={tasks} />
|
<Tasks tasks={tasks} orgName={org.name} />
|
||||||
</Spinner>
|
</Spinner>
|
||||||
)}
|
)}
|
||||||
</GetOrgResources>
|
</GetOrgResources>
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
Editable Description Component Styles
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import 'src/style/modules';
|
||||||
|
|
||||||
|
$rename-dash-title-padding: 5px;
|
||||||
|
|
||||||
|
.editable-description {
|
||||||
|
height: $form-xs-height;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-description--preview,
|
||||||
|
.input.editable-description--input > input {
|
||||||
|
font-size: $form-xs-font;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: $ix-text-font;
|
||||||
|
padding: 0 $rename-dash-title-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-description--preview,
|
||||||
|
.editable-description--input {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-description--preview {
|
||||||
|
border-radius: $radius;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
@include no-user-select();
|
||||||
|
color: $g13-mist;
|
||||||
|
transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
|
||||||
|
border: $ix-border solid transparent;
|
||||||
|
height: $form-xs-height;
|
||||||
|
line-height: $form-xs-height - $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;
|
||||||
|
// background-color: $g3-castle;
|
||||||
|
// border-color: $g3-castle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure placeholder text matches font weight of title */
|
||||||
|
.input.editable-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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
// 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/shared/components/editable_description/EditableDescription.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onUpdate: (name: string) => void
|
||||||
|
description: string
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isEditing: boolean
|
||||||
|
workingDescription: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@ErrorHandling
|
||||||
|
class EditableDescription 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="editable-description">
|
||||||
|
<ClickOutside onClickOutside={this.handleStopEditing}>
|
||||||
|
{this.input}
|
||||||
|
</ClickOutside>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editable-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}
|
||||||
|
maxLength={90}
|
||||||
|
autoFocus={true}
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onFocus={this.handleInputFocus}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
customClass="editable-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('editable-description--preview', {
|
||||||
|
untitled: description === '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditableDescription
|
|
@ -22,7 +22,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ComponentSpacer align={Alignment.Left} stretchToFit={true}>
|
<ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
|
||||||
<label className="task-form--form-label">
|
<label className="task-form--form-label">
|
||||||
{schedule === TaskSchedule.interval ? 'Interval' : 'Cron'}
|
{schedule === TaskSchedule.interval ? 'Interval' : 'Cron'}
|
||||||
</label>
|
</label>
|
||||||
|
@ -37,7 +37,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
|
||||||
/>
|
/>
|
||||||
</ComponentSpacer>
|
</ComponentSpacer>
|
||||||
|
|
||||||
<ComponentSpacer align={Alignment.Left} stretchToFit={true}>
|
<ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
|
||||||
<label className="task-form--form-label">Offset</label>
|
<label className="task-form--form-label">Offset</label>
|
||||||
<Input
|
<Input
|
||||||
name="offset"
|
name="offset"
|
||||||
|
|
Loading…
Reference in New Issue