feat(ui/labels): Add the ability to edit labels on a dashboard
Co-authored-by: Alex Paxton <thealexpaxton@gmail.com>pull/10616/head
parent
7cf643d1d5
commit
d12e94bb01
|
@ -1397,13 +1397,49 @@ paths:
|
|||
responses:
|
||||
'200':
|
||||
description: a list of all labels for a dashboard
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
label:
|
||||
$ref: "#/components/schemas/Label"
|
||||
links:
|
||||
$ref: "#/components/schemas/Links"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
'/dashboards/{dashboardID}/labels/{name}':
|
||||
delete:
|
||||
tags:
|
||||
- Dashboards
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TraceSpan'
|
||||
- in: path
|
||||
name: dashboardID
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the dashboard
|
||||
- in: path
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: name of the label
|
||||
responses:
|
||||
'200':
|
||||
description: the removed label for a dashboard
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
$ref: "#/components/schemas/Labels"
|
||||
$ref: "#/components/schemas/Label"
|
||||
links:
|
||||
$ref: "#/components/schemas/Links"
|
||||
default:
|
||||
|
|
|
@ -1110,10 +1110,10 @@ export interface InlineResponse200 {
|
|||
export interface InlineResponse2001 {
|
||||
/**
|
||||
*
|
||||
* @type {Array<Task>}
|
||||
* @type {Label}
|
||||
* @memberof InlineResponse2001
|
||||
*/
|
||||
tasks?: Array<Task>;
|
||||
label?: Label;
|
||||
/**
|
||||
*
|
||||
* @type {Links}
|
||||
|
@ -1130,14 +1130,54 @@ export interface InlineResponse2001 {
|
|||
export interface InlineResponse2002 {
|
||||
/**
|
||||
*
|
||||
* @type {Array<Run>}
|
||||
* @type {Label}
|
||||
* @memberof InlineResponse2002
|
||||
*/
|
||||
labels?: Label;
|
||||
/**
|
||||
*
|
||||
* @type {Links}
|
||||
* @memberof InlineResponse2002
|
||||
*/
|
||||
links?: Links;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface InlineResponse2003
|
||||
*/
|
||||
export interface InlineResponse2003 {
|
||||
/**
|
||||
*
|
||||
* @type {Array<Task>}
|
||||
* @memberof InlineResponse2003
|
||||
*/
|
||||
tasks?: Array<Task>;
|
||||
/**
|
||||
*
|
||||
* @type {Links}
|
||||
* @memberof InlineResponse2003
|
||||
*/
|
||||
links?: Links;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface InlineResponse2004
|
||||
*/
|
||||
export interface InlineResponse2004 {
|
||||
/**
|
||||
*
|
||||
* @type {Array<Run>}
|
||||
* @memberof InlineResponse2004
|
||||
*/
|
||||
runs?: Array<Run>;
|
||||
/**
|
||||
*
|
||||
* @type {Links}
|
||||
* @memberof InlineResponse2002
|
||||
* @memberof InlineResponse2004
|
||||
*/
|
||||
links?: Links;
|
||||
}
|
||||
|
@ -7003,6 +7043,49 @@ export const DashboardsApiAxiosParamCreator = function (configuration?: Configur
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} dashboardID ID of the dashboard
|
||||
* @param {string} name name of the label
|
||||
* @param {string} [zapTraceSpan] OpenTracing span context
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
dashboardsDashboardIDLabelsNameDelete(dashboardID: string, name: string, zapTraceSpan?: string, options: any = {}): RequestArgs {
|
||||
// verify required parameter 'dashboardID' is not null or undefined
|
||||
if (dashboardID === null || dashboardID === undefined) {
|
||||
throw new RequiredError('dashboardID','Required parameter dashboardID was null or undefined when calling dashboardsDashboardIDLabelsNameDelete.');
|
||||
}
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
if (name === null || name === undefined) {
|
||||
throw new RequiredError('name','Required parameter name was null or undefined when calling dashboardsDashboardIDLabelsNameDelete.');
|
||||
}
|
||||
const localVarPath = `/dashboards/{dashboardID}/labels/{name}`
|
||||
.replace(`{${"dashboardID"}}`, encodeURIComponent(String(dashboardID)))
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
const localVarUrlObj = url.parse(localVarPath, true);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
const localVarRequestOptions = Object.assign({ method: 'DELETE' }, baseOptions, options);
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (zapTraceSpan !== undefined && zapTraceSpan !== null) {
|
||||
localVarHeaderParameter['Zap-Trace-Span'] = String(zapTraceSpan);
|
||||
}
|
||||
|
||||
localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query);
|
||||
// fix override query string Detail: https://stackoverflow.com/a/7517673/1077943
|
||||
delete localVarUrlObj.search;
|
||||
localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers);
|
||||
|
||||
return {
|
||||
url: url.format(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary add a label to a dashboard
|
||||
|
@ -7525,6 +7608,21 @@ export const DashboardsApiFp = function(configuration?: Configuration) {
|
|||
return axios.request(axiosRequestArgs);
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} dashboardID ID of the dashboard
|
||||
* @param {string} name name of the label
|
||||
* @param {string} [zapTraceSpan] OpenTracing span context
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
dashboardsDashboardIDLabelsNameDelete(dashboardID: string, name: string, zapTraceSpan?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002> {
|
||||
const localVarAxiosArgs = DashboardsApiAxiosParamCreator(configuration).dashboardsDashboardIDLabelsNameDelete(dashboardID, name, zapTraceSpan, options);
|
||||
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = Object.assign(localVarAxiosArgs.options, {url: basePath + localVarAxiosArgs.url})
|
||||
return axios.request(axiosRequestArgs);
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary add a label to a dashboard
|
||||
|
@ -7534,7 +7632,7 @@ export const DashboardsApiFp = function(configuration?: Configuration) {
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
dashboardsDashboardIDLabelsPost(dashboardID: string, label: Label, zapTraceSpan?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse200> {
|
||||
dashboardsDashboardIDLabelsPost(dashboardID: string, label: Label, zapTraceSpan?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001> {
|
||||
const localVarAxiosArgs = DashboardsApiAxiosParamCreator(configuration).dashboardsDashboardIDLabelsPost(dashboardID, label, zapTraceSpan, options);
|
||||
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = Object.assign(localVarAxiosArgs.options, {url: basePath + localVarAxiosArgs.url})
|
||||
|
@ -7761,6 +7859,17 @@ export const DashboardsApiFactory = function (configuration?: Configuration, bas
|
|||
dashboardsDashboardIDLabelsGet(dashboardID: string, zapTraceSpan?: string, options?: any) {
|
||||
return DashboardsApiFp(configuration).dashboardsDashboardIDLabelsGet(dashboardID, zapTraceSpan, options)(axios, basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} dashboardID ID of the dashboard
|
||||
* @param {string} name name of the label
|
||||
* @param {string} [zapTraceSpan] OpenTracing span context
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
dashboardsDashboardIDLabelsNameDelete(dashboardID: string, name: string, zapTraceSpan?: string, options?: any) {
|
||||
return DashboardsApiFp(configuration).dashboardsDashboardIDLabelsNameDelete(dashboardID, name, zapTraceSpan, options)(axios, basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary add a label to a dashboard
|
||||
|
@ -7972,6 +8081,19 @@ export class DashboardsApi extends BaseAPI {
|
|||
return DashboardsApiFp(this.configuration).dashboardsDashboardIDLabelsGet(dashboardID, zapTraceSpan, options)(this.axios, this.basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} dashboardID ID of the dashboard
|
||||
* @param {string} name name of the label
|
||||
* @param {string} [zapTraceSpan] OpenTracing span context
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DashboardsApi
|
||||
*/
|
||||
public dashboardsDashboardIDLabelsNameDelete(dashboardID: string, name: string, zapTraceSpan?: string, options?: any) {
|
||||
return DashboardsApiFp(this.configuration).dashboardsDashboardIDLabelsNameDelete(dashboardID, name, zapTraceSpan, options)(this.axios, this.basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary add a label to a dashboard
|
||||
|
@ -11613,7 +11735,7 @@ export const TasksApiFp = function(configuration?: Configuration) {
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
tasksGet(after?: string, user?: string, org?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001> {
|
||||
tasksGet(after?: string, user?: string, org?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2003> {
|
||||
const localVarAxiosArgs = TasksApiAxiosParamCreator(configuration).tasksGet(after, user, org, options);
|
||||
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = Object.assign(localVarAxiosArgs.options, {url: basePath + localVarAxiosArgs.url})
|
||||
|
@ -11790,7 +11912,7 @@ export const TasksApiFp = function(configuration?: Configuration) {
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
tasksTaskIDRunsGet(taskID: string, after?: string, limit?: number, afterTime?: Date, beforeTime?: Date, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002> {
|
||||
tasksTaskIDRunsGet(taskID: string, after?: string, limit?: number, afterTime?: Date, beforeTime?: Date, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004> {
|
||||
const localVarAxiosArgs = TasksApiAxiosParamCreator(configuration).tasksTaskIDRunsGet(taskID, after, limit, afterTime, beforeTime, options);
|
||||
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = Object.assign(localVarAxiosArgs.options, {url: basePath + localVarAxiosArgs.url})
|
||||
|
|
|
@ -203,3 +203,13 @@
|
|||
.index-list--labels {
|
||||
margin-left: $ix-marg-b;
|
||||
}
|
||||
|
||||
.index-list--row-cell .index-list--cell a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.index-list--resource-name {
|
||||
margin-left: 6px;
|
||||
margin-right: $ix-marg-a + $ix-marg-b;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
|
|
@ -21,23 +21,6 @@ $label-margin: 1px;
|
|||
}
|
||||
}
|
||||
|
||||
.label--container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label--container-margin {
|
||||
width: calc(100% + #{$label-margin * 2});
|
||||
position: relative;
|
||||
left: $label-margin * -2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $label-margin;
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.label--clickable {
|
||||
&, &:hover {
|
||||
cursor: pointer;
|
||||
|
@ -118,68 +101,3 @@ $label-margin: 1px;
|
|||
.label--lg {
|
||||
@include labelSizeModifier($form-lg-font, $form-lg-padding, $form-lg-height);
|
||||
}
|
||||
|
||||
/*
|
||||
Additional Labels Indicator
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.additional-labels {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
font-weight: 700;
|
||||
margin: $label-margin;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
background-color: rgba($g6-smoke, 0.5);
|
||||
color: $g11-sidewalk;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
}
|
||||
|
||||
.label--tooltip {
|
||||
z-index: 9999;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition-property: all;
|
||||
}
|
||||
|
||||
.label--tooltip-container {
|
||||
@extend %drop-shadow;
|
||||
opacity: 0;
|
||||
background-color: $g0-obsidian;
|
||||
border-radius: $radius;
|
||||
padding: $ix-marg-c;
|
||||
margin-top: $ix-marg-b + $ix-border;
|
||||
transition: opacity 0.25s ease;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border: $ix-marg-b solid transparent;
|
||||
border-bottom-color: $g0-obsidian;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip-container {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Label Container Styles
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import 'src/style/modules';
|
||||
|
||||
$label-margin: 1px;
|
||||
$label-edit-button-diameter: 18px;
|
||||
|
||||
.label--container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label--container-margin {
|
||||
width: calc(100% + #{$label-margin * 2});
|
||||
position: relative;
|
||||
left: $label-margin * -2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $label-margin;
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Additional Labels Indicator
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.additional-labels {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
font-weight: 700;
|
||||
margin: $label-margin;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
background-color: rgba($g6-smoke, 0.5);
|
||||
color: $g11-sidewalk;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
}
|
||||
|
||||
.label--tooltip {
|
||||
z-index: 9999;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition-property: all;
|
||||
}
|
||||
|
||||
.label--tooltip-container {
|
||||
@extend %drop-shadow;
|
||||
opacity: 0;
|
||||
background-color: $g0-obsidian;
|
||||
border-radius: $radius;
|
||||
padding: $ix-marg-c;
|
||||
margin-top: $ix-marg-b + $ix-border;
|
||||
transition: opacity 0.25s ease;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border: $ix-marg-b solid transparent;
|
||||
border-bottom-color: $g0-obsidian;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
> .label {
|
||||
margin: $label-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.additional-labels:hover .label--tooltip-container {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
Edit Labels on Resource
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.label--edit-button {
|
||||
width: $label-edit-button-diameter;
|
||||
height: $label-edit-button-diameter;
|
||||
margin: $label-margin;
|
||||
position: relative;
|
||||
transition: opacity 0.25s ease;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
background-color: $g20-white;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
z-index: 3;
|
||||
transition: width 0.25s ease;
|
||||
height: $ix-border;
|
||||
width: $label-edit-button-diameter - $ix-marg-b;
|
||||
border-radius: $ix-border / 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: translate(-50%,-50%) rotate(90deg);
|
||||
}
|
||||
|
||||
> .button.button-sm {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: $label-edit-button-diameter;
|
||||
height: $label-edit-button-diameter;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
color: transparent;
|
||||
transition:
|
||||
background-color 0.25s ease,
|
||||
border-color 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
height 0.25s ease,
|
||||
width 0.25s ease;
|
||||
|
||||
> .button-icon {
|
||||
font-size: $form-sm-font;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .button.button-sm {
|
||||
width: $form-sm-height - $ix-marg-a;
|
||||
height: $form-sm-height - $ix-marg-a;
|
||||
}
|
||||
|
||||
&:hover:before,
|
||||
&:hover:after {
|
||||
width: $form-sm-height - $ix-marg-c;
|
||||
}
|
||||
}
|
||||
|
||||
/* When used inside an index list, hide until row hover */
|
||||
.index-list--row-cell .label--edit-button {
|
||||
opacity: 0;
|
||||
}
|
||||
.index-list--row-cell:hover .label--edit-button {
|
||||
opacity: 1;
|
||||
}
|
|
@ -1,19 +1,28 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import LabelTooltip from 'src/clockface/components/label/LabelTooltip'
|
||||
import Button from 'src/clockface/components/Button/index'
|
||||
import {ButtonShape, IconFont, ComponentColor} from 'src/clockface/types'
|
||||
|
||||
// Styles
|
||||
import 'src/clockface/components/label/LabelContainer.scss'
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[]
|
||||
children?: JSX.Element[]
|
||||
className?: string
|
||||
limitChildCount?: number
|
||||
resourceName?: string
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
class LabelContainer extends Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
limitChildCount: 999,
|
||||
resourceName: 'this resource',
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -28,19 +37,39 @@ class LabelContainer extends Component<Props> {
|
|||
<div className="label--container-margin">
|
||||
{this.children}
|
||||
{this.additionalChildrenIndicator}
|
||||
{this.editButton}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get children(): JSX.Element[] {
|
||||
private get sortedChildren(): JSX.Element[] {
|
||||
const {children} = this.props
|
||||
|
||||
if (children && React.Children.count(children) > 1) {
|
||||
return children.sort((a: JSX.Element, b: JSX.Element) => {
|
||||
const textA = a.props.name.toUpperCase()
|
||||
const textB = b.props.name.toUpperCase()
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0
|
||||
})
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
private get children(): JSX.Element[] | JSX.Element {
|
||||
const {children, limitChildCount} = this.props
|
||||
|
||||
return React.Children.map(children, (child: JSX.Element, i: number) => {
|
||||
if (i < limitChildCount) {
|
||||
return child
|
||||
}
|
||||
})
|
||||
if (children) {
|
||||
return React.Children.map(
|
||||
this.sortedChildren,
|
||||
(child: JSX.Element, i: number) => {
|
||||
if (i < limitChildCount) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get additionalChildrenIndicator(): JSX.Element {
|
||||
|
@ -53,7 +82,29 @@ class LabelContainer extends Component<Props> {
|
|||
return (
|
||||
<div className="additional-labels">
|
||||
+{additionalCount} more
|
||||
<LabelTooltip labels={children} />
|
||||
<LabelTooltip labels={this.sortedChildren.slice(limitChildCount)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get editButton(): JSX.Element {
|
||||
const {onEdit, children, resourceName} = this.props
|
||||
|
||||
const titleText = React.Children.count(children)
|
||||
? `Edit Labels for ${resourceName}`
|
||||
: `Add Labels to ${resourceName}`
|
||||
|
||||
if (onEdit) {
|
||||
return (
|
||||
<div className="label--edit-button">
|
||||
<Button
|
||||
color={ComponentColor.Primary}
|
||||
titleText={titleText}
|
||||
onClick={onEdit}
|
||||
shape={ButtonShape.Square}
|
||||
icon={IconFont.Plus}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,13 +5,14 @@ import _ from 'lodash'
|
|||
// Components
|
||||
import Input from 'src/clockface/components/inputs/Input'
|
||||
import Button from 'src/clockface/components/Button'
|
||||
import Label, {LabelType} from 'src/clockface/components/label/Label'
|
||||
import Label from 'src/clockface/components/label/Label'
|
||||
import LabelContainer from 'src/clockface/components/label/LabelContainer'
|
||||
import LabelSelectorMenu from 'src/clockface/components/label/LabelSelectorMenu'
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
|
||||
// Types
|
||||
import {ComponentSize} from 'src/clockface/types'
|
||||
import {Label as LabelType} from 'src/api'
|
||||
|
||||
// Styles
|
||||
import './LabelSelector.scss'
|
||||
|
@ -64,7 +65,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {resourceType} = this.props
|
||||
const {resourceType, inputSize} = this.props
|
||||
const {filterValue} = this.state
|
||||
|
||||
return (
|
||||
|
@ -77,6 +78,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
onFocus={this.handleStartSuggesting}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
size={inputSize}
|
||||
/>
|
||||
{this.suggestionMenu}
|
||||
</div>
|
||||
|
@ -97,12 +99,12 @@ class LabelSelector extends Component<Props, State> {
|
|||
<LabelContainer className="label-selector--selected">
|
||||
{selectedLabels.map(label => (
|
||||
<Label
|
||||
key={label.id}
|
||||
key={label.name}
|
||||
name={label.name}
|
||||
id={label.id}
|
||||
colorHex={label.colorHex}
|
||||
id={label.name}
|
||||
colorHex={label.properties.color}
|
||||
onDelete={this.handleDelete}
|
||||
description={label.description}
|
||||
description={label.properties.description}
|
||||
/>
|
||||
))}
|
||||
</LabelContainer>
|
||||
|
@ -138,7 +140,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
private handleAddLabel = (labelID: string): void => {
|
||||
const {onAddLabel, allLabels} = this.props
|
||||
|
||||
const label = allLabels.find(label => label.id === labelID)
|
||||
const label = allLabels.find(label => label.name === labelID)
|
||||
|
||||
onAddLabel(label)
|
||||
this.handleStopSuggesting()
|
||||
|
@ -159,7 +161,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
})
|
||||
}
|
||||
|
||||
const highlightedID = this.availableLabels[0].id
|
||||
const highlightedID = this.availableLabels[0].name
|
||||
this.setState({isSuggesting: true, highlightedID, filterValue: ''})
|
||||
}
|
||||
|
||||
|
@ -198,11 +200,11 @@ class LabelSelector extends Component<Props, State> {
|
|||
})
|
||||
|
||||
const highlightedIDAvailable = filteredLabels.find(
|
||||
al => al.id === highlightedID
|
||||
al => al.name === highlightedID
|
||||
)
|
||||
|
||||
if (!highlightedIDAvailable && filteredLabels.length) {
|
||||
highlightedID = filteredLabels[0].id
|
||||
highlightedID = filteredLabels[0].name
|
||||
}
|
||||
|
||||
this.setState({filterValue, filteredLabels, highlightedID})
|
||||
|
@ -218,7 +220,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
private handleDelete = (labelID: string): void => {
|
||||
const {onRemoveLabel, selectedLabels} = this.props
|
||||
|
||||
const label = selectedLabels.find(l => l.id === labelID)
|
||||
const label = selectedLabels.find(l => l.name === labelID)
|
||||
|
||||
onRemoveLabel(label)
|
||||
}
|
||||
|
@ -233,7 +235,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
|
||||
const highlightedIndex = _.findIndex(
|
||||
availableLabels,
|
||||
label => label.id === highlightedID
|
||||
label => label.name === highlightedID
|
||||
)
|
||||
|
||||
const adjacentIndex = Math.min(
|
||||
|
@ -241,7 +243,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
availableLabels.length - 1
|
||||
)
|
||||
|
||||
const adjacentID = availableLabels[adjacentIndex].id
|
||||
const adjacentID = availableLabels[adjacentIndex].name
|
||||
|
||||
this.setState({highlightedID: adjacentID})
|
||||
}
|
||||
|
|
|
@ -4,14 +4,17 @@ import _ from 'lodash'
|
|||
|
||||
// Components
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
import {LabelType} from 'src/clockface/components/label/Label'
|
||||
import LabelSelectorMenuItem from 'src/clockface/components/label/LabelSelectorMenuItem'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Label} from 'src/api'
|
||||
|
||||
interface Props {
|
||||
highlightItemID: string
|
||||
filteredLabels: LabelType[]
|
||||
filteredLabels: Label[]
|
||||
onItemClick: (labelID: string) => void
|
||||
onItemHighlight: (labelID: string) => void
|
||||
allLabelsUsed: boolean
|
||||
|
@ -40,12 +43,12 @@ class LabelSelectorMenu extends Component<Props> {
|
|||
if (filteredLabels.length) {
|
||||
return filteredLabels.map(label => (
|
||||
<LabelSelectorMenuItem
|
||||
highlighted={highlightItemID === label.id}
|
||||
key={label.id}
|
||||
highlighted={highlightItemID === label.name}
|
||||
key={label.name}
|
||||
name={label.name}
|
||||
id={label.id}
|
||||
description={label.description}
|
||||
colorHex={label.colorHex}
|
||||
id={label.name}
|
||||
description={label.properties.description}
|
||||
colorHex={label.properties.color}
|
||||
onClick={onItemClick}
|
||||
onHighlight={onItemHighlight}
|
||||
/>
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
updateCells as updateCellsAJAX,
|
||||
addCell as addCellAJAX,
|
||||
deleteCell as deleteCellAJAX,
|
||||
addDashboardLabels as addDashboardLabelsAJAX,
|
||||
removeDashboardLabels as removeDashboardLabelsAJAX,
|
||||
} from 'src/dashboards/apis/v2'
|
||||
import {createView as createViewAJAX} from 'src/dashboards/apis/v2/view'
|
||||
|
||||
|
@ -36,7 +38,7 @@ import * as copy from 'src/shared/copy/notifications'
|
|||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {PublishNotificationAction} from 'src/types/actions/notifications'
|
||||
import {Dashboard, Cell} from 'src/api'
|
||||
import {Dashboard, Cell, Label} from 'src/api'
|
||||
import {NewView} from 'src/types/v2/dashboards'
|
||||
|
||||
export enum ActionTypes {
|
||||
|
@ -46,6 +48,8 @@ export enum ActionTypes {
|
|||
DeleteDashboardFailed = 'DELETE_DASHBOARD_FAILED',
|
||||
UpdateDashboard = 'UPDATE_DASHBOARD',
|
||||
DeleteCell = 'DELETE_CELL',
|
||||
AddDashboardLabels = 'ADD_DASHBOARD_LABELS',
|
||||
RemoveDashboardLabels = 'REMOVE_DASHBOARD_LABELS',
|
||||
}
|
||||
|
||||
export type Action =
|
||||
|
@ -58,6 +62,8 @@ export type Action =
|
|||
| SetViewAction
|
||||
| DeleteTimeRangeAction
|
||||
| DeleteDashboardFailedAction
|
||||
| AddDashboardLabelsAction
|
||||
| RemoveDashboardLabelsAction
|
||||
|
||||
interface DeleteCellAction {
|
||||
type: ActionTypes.DeleteCell
|
||||
|
@ -102,6 +108,22 @@ interface LoadDashboardAction {
|
|||
}
|
||||
}
|
||||
|
||||
interface AddDashboardLabelsAction {
|
||||
type: ActionTypes.AddDashboardLabels
|
||||
payload: {
|
||||
dashboardID: string
|
||||
labels: Label[]
|
||||
}
|
||||
}
|
||||
|
||||
interface RemoveDashboardLabelsAction {
|
||||
type: ActionTypes.RemoveDashboardLabels
|
||||
payload: {
|
||||
dashboardID: string
|
||||
labels: Label[]
|
||||
}
|
||||
}
|
||||
|
||||
// Action Creators
|
||||
|
||||
export const updateDashboard = (
|
||||
|
@ -147,6 +169,22 @@ export const deleteCell = (
|
|||
payload: {dashboard, cell},
|
||||
})
|
||||
|
||||
export const addDashboardLabels = (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
): AddDashboardLabelsAction => ({
|
||||
type: ActionTypes.AddDashboardLabels,
|
||||
payload: {dashboardID, labels},
|
||||
})
|
||||
|
||||
export const removeDashboardLabels = (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
): RemoveDashboardLabelsAction => ({
|
||||
type: ActionTypes.RemoveDashboardLabels,
|
||||
payload: {dashboardID, labels},
|
||||
})
|
||||
|
||||
// Thunks
|
||||
|
||||
export const getDashboardsAsync = () => async (
|
||||
|
@ -317,3 +355,30 @@ export const copyDashboardCellAsync = (
|
|||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const addDashboardLabelsAsync = (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
) => async (dispatch: Dispatch<Action>) => {
|
||||
try {
|
||||
const newLabels = await addDashboardLabelsAJAX(dashboardID, labels)
|
||||
|
||||
dispatch(addDashboardLabels(dashboardID, newLabels))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(notify(copy.addDashboardLabelFailed()))
|
||||
}
|
||||
}
|
||||
|
||||
export const removeDashboardLabelsAsync = (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
) => async (dispatch: Dispatch<Action>) => {
|
||||
try {
|
||||
await removeDashboardLabelsAJAX(dashboardID, labels)
|
||||
dispatch(removeDashboardLabels(dashboardID, labels))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(notify(copy.removedDashboardLabelFailed()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import {dashboardsAPI, cellsAPI} from 'src/utils/api'
|
||||
|
||||
// Types
|
||||
import {Dashboard, Cell, CreateCell} from 'src/api'
|
||||
import {Dashboard, Cell, CreateCell, Label} from 'src/api'
|
||||
import {DashboardSwitcherLinks} from 'src/types/v2/dashboards'
|
||||
|
||||
// Utils
|
||||
|
@ -83,3 +83,35 @@ export const deleteCell = async (
|
|||
): Promise<void> => {
|
||||
await cellsAPI.dashboardsDashboardIDCellsCellIDDelete(dashboardID, cell.id)
|
||||
}
|
||||
|
||||
export const addDashboardLabels = async (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
): Promise<Label[]> => {
|
||||
const addedLabels = await Promise.all(
|
||||
labels.map(async label => {
|
||||
const {data} = await dashboardsAPI.dashboardsDashboardIDLabelsPost(
|
||||
dashboardID,
|
||||
label
|
||||
)
|
||||
return data.label
|
||||
})
|
||||
)
|
||||
|
||||
return addedLabels
|
||||
}
|
||||
|
||||
export const removeDashboardLabels = async (
|
||||
dashboardID: string,
|
||||
labels: Label[]
|
||||
): Promise<void> => {
|
||||
await Promise.all(
|
||||
labels.map(async label => {
|
||||
const {data} = await dashboardsAPI.dashboardsDashboardIDLabelsNameDelete(
|
||||
dashboardID,
|
||||
label.name
|
||||
)
|
||||
return data
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import {
|
||||
OverlayContainer,
|
||||
OverlayHeading,
|
||||
OverlayBody,
|
||||
LabelSelector,
|
||||
ComponentSize,
|
||||
ComponentColor,
|
||||
Button,
|
||||
Grid,
|
||||
Form,
|
||||
ComponentStatus,
|
||||
} from 'src/clockface'
|
||||
import FetchLabels from 'src/shared/components/FetchLabels'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
addDashboardLabelsAsync,
|
||||
removeDashboardLabelsAsync,
|
||||
} from 'src/dashboards/actions/v2'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Dashboard} from 'src/types/v2'
|
||||
import {Label} from 'src/api'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
dashboard: Dashboard
|
||||
onDismissOverlay: () => void
|
||||
onAddDashboardLabels: typeof addDashboardLabelsAsync
|
||||
onRemoveDashboardLabels: typeof removeDashboardLabelsAsync
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedLabels: Label[]
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class EditDashboardLabelsOverlay extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selectedLabels: props.dashboard.labels,
|
||||
loading: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onDismissOverlay, dashboard} = this.props
|
||||
const {selectedLabels} = this.state
|
||||
|
||||
return (
|
||||
<OverlayContainer maxWidth={720}>
|
||||
<OverlayHeading title="Manage Labels" onDismiss={onDismissOverlay} />
|
||||
<OverlayBody>
|
||||
<Form>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<Form.Element label="">
|
||||
<Form.Box>
|
||||
<FetchLabels>
|
||||
{labels => (
|
||||
<LabelSelector
|
||||
inputSize={ComponentSize.Medium}
|
||||
allLabels={labels}
|
||||
resourceType={dashboard.name}
|
||||
selectedLabels={selectedLabels}
|
||||
onAddLabel={this.handleAddLabel}
|
||||
onRemoveLabel={this.handleRemoveLabel}
|
||||
onRemoveAllLabels={this.handleRemoveAllLabels}
|
||||
/>
|
||||
)}
|
||||
</FetchLabels>
|
||||
</Form.Box>
|
||||
</Form.Element>
|
||||
<Form.Footer>
|
||||
<Button text="Cancel" onClick={onDismissOverlay} />
|
||||
<Button
|
||||
color={ComponentColor.Success}
|
||||
text="Save Changes"
|
||||
onClick={this.handleSaveLabels}
|
||||
status={this.buttonStatus}
|
||||
/>
|
||||
</Form.Footer>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Form>
|
||||
</OverlayBody>
|
||||
</OverlayContainer>
|
||||
)
|
||||
}
|
||||
|
||||
private get buttonStatus(): ComponentStatus {
|
||||
if (this.changes.isChanged) {
|
||||
if (this.state.loading === RemoteDataState.Loading) {
|
||||
return ComponentStatus.Loading
|
||||
}
|
||||
|
||||
return ComponentStatus.Default
|
||||
}
|
||||
|
||||
return ComponentStatus.Disabled
|
||||
}
|
||||
|
||||
private get changes(): {
|
||||
isChanged: boolean
|
||||
removedLabels: Label[]
|
||||
addedLabels: Label[]
|
||||
} {
|
||||
const {dashboard} = this.props
|
||||
const {selectedLabels} = this.state
|
||||
|
||||
const intersection = _.intersectionBy(
|
||||
dashboard.labels,
|
||||
selectedLabels,
|
||||
'name'
|
||||
)
|
||||
const removedLabels = _.differenceBy(dashboard.labels, intersection, 'name')
|
||||
const addedLabels = _.differenceBy(selectedLabels, intersection, 'name')
|
||||
|
||||
return {
|
||||
isChanged: !!removedLabels.length || !!addedLabels.length,
|
||||
addedLabels,
|
||||
removedLabels,
|
||||
}
|
||||
}
|
||||
|
||||
private handleRemoveAllLabels = (): void => {
|
||||
this.setState({selectedLabels: []})
|
||||
}
|
||||
|
||||
private handleRemoveLabel = (label: Label): void => {
|
||||
const selectedLabels = this.state.selectedLabels.filter(
|
||||
l => l.name !== label.name
|
||||
)
|
||||
|
||||
this.setState({selectedLabels})
|
||||
}
|
||||
|
||||
private handleAddLabel = (label: Label): void => {
|
||||
const selectedLabels = [...this.state.selectedLabels, label]
|
||||
|
||||
this.setState({selectedLabels})
|
||||
}
|
||||
|
||||
private handleSaveLabels = async (): Promise<void> => {
|
||||
const {
|
||||
onAddDashboardLabels,
|
||||
onRemoveDashboardLabels,
|
||||
dashboard,
|
||||
onDismissOverlay,
|
||||
} = this.props
|
||||
|
||||
const {addedLabels, removedLabels} = this.changes
|
||||
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
|
||||
if (addedLabels.length) {
|
||||
await onAddDashboardLabels(dashboard.id, addedLabels)
|
||||
}
|
||||
|
||||
if (removedLabels.length) {
|
||||
await onRemoveDashboardLabels(dashboard.id, removedLabels)
|
||||
}
|
||||
|
||||
this.setState({loading: RemoteDataState.Done})
|
||||
|
||||
onDismissOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
export default EditDashboardLabelsOverlay
|
|
@ -21,6 +21,7 @@ interface Props {
|
|||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
searchTerm: string
|
||||
}
|
||||
|
@ -36,6 +37,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
|||
defaultDashboardLink,
|
||||
onSetDefaultDashboard,
|
||||
onUpdateDashboard,
|
||||
onEditLabels,
|
||||
searchTerm,
|
||||
} = this.props
|
||||
|
||||
|
@ -51,6 +53,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
|||
defaultDashboardLink={defaultDashboardLink}
|
||||
onSetDefaultDashboard={onSetDefaultDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
onEditLabels={onEditLabels}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
IconFont,
|
||||
} from 'src/clockface'
|
||||
import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOverlay'
|
||||
import EditDashboardLabelsOverlay from 'src/dashboards/components/EditDashboardLabelsOverlay'
|
||||
|
||||
// Utils
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
@ -29,6 +30,8 @@ import {
|
|||
importDashboardAsync,
|
||||
deleteDashboardAsync,
|
||||
updateDashboardAsync,
|
||||
addDashboardLabelsAsync,
|
||||
removeDashboardLabelsAsync,
|
||||
} from 'src/dashboards/actions/v2'
|
||||
import {setDefaultDashboard} from 'src/shared/actions/links'
|
||||
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
|
||||
|
@ -48,14 +51,12 @@ import {DEFAULT_DASHBOARD_NAME} from 'src/dashboards/constants/index'
|
|||
import {Notification} from 'src/types/notifications'
|
||||
import {DashboardFile} from 'src/types/v2/dashboards'
|
||||
import {Cell, Dashboard} from 'src/api'
|
||||
import {Links} from 'src/types/v2'
|
||||
import {Links, AppState} from 'src/types/v2'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
router: InjectedRouter
|
||||
links: Links
|
||||
interface DispatchProps {
|
||||
handleSetDefaultDashboard: typeof setDefaultDashboard
|
||||
handleGetDashboards: typeof getDashboardsAsync
|
||||
handleDeleteDashboard: typeof deleteDashboardAsync
|
||||
|
@ -63,12 +64,26 @@ interface Props {
|
|||
handleUpdateDashboard: typeof updateDashboardAsync
|
||||
notify: (message: Notification) => void
|
||||
retainRangesDashTimeV1: (dashboardIDs: string[]) => void
|
||||
onAddDashboardLabels: typeof addDashboardLabelsAsync
|
||||
onRemoveDashboardLabels: typeof removeDashboardLabelsAsync
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
links: Links
|
||||
dashboards: Dashboard[]
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
router: InjectedRouter
|
||||
}
|
||||
|
||||
type Props = DispatchProps & StateProps & OwnProps
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
isImportingDashboard: boolean
|
||||
isEditingDashboardLabels: boolean
|
||||
dashboardLabelsEdit: Dashboard
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -79,6 +94,8 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
this.state = {
|
||||
searchTerm: '',
|
||||
isImportingDashboard: false,
|
||||
isEditingDashboardLabels: false,
|
||||
dashboardLabelsEdit: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,12 +141,14 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
onCloneDashboard={this.handleCloneDashboard}
|
||||
onExportDashboard={this.handleExportDashboard}
|
||||
onUpdateDashboard={handleUpdateDashboard}
|
||||
onEditLabels={this.handleStartEditingLabels}
|
||||
notify={notify}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
{this.renderImportOverlay}
|
||||
{this.renderLabelEditorOverlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -251,9 +270,33 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
|
||||
private handleStartEditingLabels = (dashboardLabelsEdit: Dashboard): void => {
|
||||
this.setState({dashboardLabelsEdit, isEditingDashboardLabels: true})
|
||||
}
|
||||
|
||||
private handleStopEditingLabels = (): void => {
|
||||
this.setState({isEditingDashboardLabels: false})
|
||||
}
|
||||
|
||||
private get renderLabelEditorOverlay(): JSX.Element {
|
||||
const {onAddDashboardLabels, onRemoveDashboardLabels} = this.props
|
||||
const {isEditingDashboardLabels, dashboardLabelsEdit} = this.state
|
||||
|
||||
return (
|
||||
<OverlayTechnology visible={isEditingDashboardLabels}>
|
||||
<EditDashboardLabelsOverlay
|
||||
dashboard={dashboardLabelsEdit}
|
||||
onDismissOverlay={this.handleStopEditingLabels}
|
||||
onAddDashboardLabels={onAddDashboardLabels}
|
||||
onRemoveDashboardLabels={onRemoveDashboardLabels}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = state => {
|
||||
const mstp = (state: AppState): StateProps => {
|
||||
const {dashboards, links} = state
|
||||
|
||||
return {
|
||||
|
@ -262,7 +305,7 @@ const mstp = state => {
|
|||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
const mdtp: DispatchProps = {
|
||||
notify: notifyAction,
|
||||
handleSetDefaultDashboard: setDefaultDashboard,
|
||||
handleGetDashboards: getDashboardsAsync,
|
||||
|
@ -270,9 +313,11 @@ const mdtp = {
|
|||
handleImportDashboard: importDashboardAsync,
|
||||
handleUpdateDashboard: updateDashboardAsync,
|
||||
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
|
||||
onAddDashboardLabels: addDashboardLabelsAsync,
|
||||
onRemoveDashboardLabels: removeDashboardLabelsAsync,
|
||||
}
|
||||
|
||||
export default connect(
|
||||
export default connect<StateProps, DispatchProps, OwnProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(DashboardIndex)
|
||||
|
|
|
@ -30,6 +30,7 @@ interface Props {
|
|||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onSetDefaultDashboard: (dashboardLink: string) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
}
|
||||
|
||||
interface DatedDashboard extends Dashboard {
|
||||
|
@ -104,6 +105,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
|||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onUpdateDashboard,
|
||||
onEditLabels,
|
||||
} = this.props
|
||||
|
||||
const {sortKey, sortDirection} = this.state
|
||||
|
@ -122,6 +124,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
|||
onExportDashboard={onExportDashboard}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
onEditLabels={onEditLabels}
|
||||
/>
|
||||
)}
|
||||
</SortingHat>
|
||||
|
|
|
@ -16,6 +16,7 @@ const setup = (override = {}) => {
|
|||
onCloneDashboard: jest.fn(),
|
||||
onExportDashboard: jest.fn(),
|
||||
onUpdateDashboard: jest.fn(),
|
||||
onEditLabels: jest.fn(),
|
||||
...override,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components
|
||||
import {
|
||||
|
@ -32,6 +33,7 @@ interface Props {
|
|||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||
|
@ -90,14 +92,24 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
const {dashboard} = this.props
|
||||
|
||||
if (!dashboard.labels.length) {
|
||||
return
|
||||
return (
|
||||
<Label.Container
|
||||
limitChildCount={4}
|
||||
className="index-list--labels"
|
||||
onEdit={this.handleEditLabels}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Label.Container limitChildCount={4} className="index-list--labels">
|
||||
<Label.Container
|
||||
limitChildCount={4}
|
||||
className="index-list--labels"
|
||||
onEdit={this.handleEditLabels}
|
||||
>
|
||||
{dashboard.labels.map(label => (
|
||||
<Label
|
||||
key={label.resourceID}
|
||||
key={`${label.resourceID}-${label.name}`}
|
||||
id={label.resourceID}
|
||||
colorHex={label.properties.color}
|
||||
name={label.name}
|
||||
|
@ -123,6 +135,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
|
||||
private handleEditLabels = () => {
|
||||
const {dashboard, onEditLabels} = this.props
|
||||
onEditLabels(dashboard)
|
||||
}
|
||||
|
||||
private get name(): string {
|
||||
const {dashboard} = this.props
|
||||
|
||||
|
@ -132,9 +149,12 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
private get nameClassName(): string {
|
||||
const {dashboard} = this.props
|
||||
|
||||
if (dashboard.name === '' || dashboard.name === DEFAULT_DASHBOARD_NAME) {
|
||||
return 'untitled-name'
|
||||
}
|
||||
const dashboardIsUntitled =
|
||||
dashboard.name === '' || dashboard.name === DEFAULT_DASHBOARD_NAME
|
||||
|
||||
return classnames('index-list--resource-name', {
|
||||
'untitled-name': dashboardIsUntitled,
|
||||
})
|
||||
}
|
||||
|
||||
private handleUpdateDescription = (description: string): void => {
|
||||
|
|
|
@ -13,6 +13,7 @@ interface Props {
|
|||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onEditLabels: (dashboard: Dashboard) => void
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||
|
@ -23,6 +24,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
|||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onUpdateDashboard,
|
||||
onEditLabels,
|
||||
} = this.props
|
||||
|
||||
return dashboards.map(d => (
|
||||
|
@ -33,6 +35,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
|||
onCloneDashboard={onCloneDashboard}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
onEditLabels={onEditLabels}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -8,10 +8,13 @@ import {
|
|||
deleteDashboard,
|
||||
updateDashboard,
|
||||
deleteCell,
|
||||
addDashboardLabels,
|
||||
removeDashboardLabels,
|
||||
} from 'src/dashboards/actions/v2/'
|
||||
|
||||
// Resources
|
||||
import {dashboard} from 'src/dashboards/resources'
|
||||
import {labels} from 'mocks/dummyData'
|
||||
|
||||
describe('dashboards reducer', () => {
|
||||
it('can load the dashboards', () => {
|
||||
|
@ -58,4 +61,26 @@ describe('dashboards reducer', () => {
|
|||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can add labels to a dashboard', () => {
|
||||
const dashboardWithoutLabels = {...dashboard, labels: []}
|
||||
const expected = [{...dashboard, labels}]
|
||||
const actual = reducer(
|
||||
[dashboardWithoutLabels],
|
||||
addDashboardLabels(dashboardWithoutLabels.id, labels)
|
||||
)
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can delete labels from a dashboard', () => {
|
||||
const dashboardWithLabels = {...dashboard, labels}
|
||||
const expected = [{...dashboard, labels: []}]
|
||||
const actual = reducer(
|
||||
[dashboardWithLabels],
|
||||
removeDashboardLabels(dashboardWithLabels.id, labels)
|
||||
)
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -48,6 +48,36 @@ export default (state: State = [], action: Action): State => {
|
|||
|
||||
return [...newState]
|
||||
}
|
||||
|
||||
case ActionTypes.AddDashboardLabels: {
|
||||
const {dashboardID, labels} = action.payload
|
||||
|
||||
const newState = state.map(d => {
|
||||
if (d.id === dashboardID) {
|
||||
return {...d, labels: [...d.labels, ...labels]}
|
||||
}
|
||||
return d
|
||||
})
|
||||
|
||||
return [...newState]
|
||||
}
|
||||
|
||||
case ActionTypes.RemoveDashboardLabels: {
|
||||
const {dashboardID, labels} = action.payload
|
||||
|
||||
const newState = state.map(d => {
|
||||
if (d.id === dashboardID) {
|
||||
const updatedLabels = d.labels.filter(l => {
|
||||
return !labels.includes(l)
|
||||
})
|
||||
return {...d, labels: updatedLabels}
|
||||
}
|
||||
return d
|
||||
})
|
||||
|
||||
return [...newState]
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import {Label} from 'src/api'
|
||||
// import {labelsAPI} from 'src/utils/api'
|
||||
// TODO: use actual API when ready
|
||||
|
||||
const mockLabels = [
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'Oomph',
|
||||
properties: {color: '#334455', description: 'this is a description'},
|
||||
},
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'TROGDOOOOORRRRRRRRR',
|
||||
properties: {color: '#44ffcc', description: 'this is a description'},
|
||||
},
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'ZZYXX',
|
||||
properties: {color: '#ff33ff', description: 'this is a description'},
|
||||
},
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'labeldawg',
|
||||
properties: {color: '#ffb3b3', description: 'this is a description'},
|
||||
},
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'porphyria',
|
||||
properties: {color: '#ff0054', description: 'this is a description'},
|
||||
},
|
||||
{
|
||||
resourceID: '0336b93e5b791000',
|
||||
name: 'snakes!',
|
||||
properties: {color: '#44ff44', description: 'this is a description'},
|
||||
},
|
||||
]
|
||||
|
||||
export const getLabels = async (): Promise<Label[]> => {
|
||||
try {
|
||||
// const {data} = await labelsAPI.labelsGet()
|
||||
// return data.labels
|
||||
return mockLabels
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import {EmptyState} from 'src/clockface'
|
||||
|
||||
// APIs
|
||||
import {getLabels} from 'src/shared/apis/v2/labels'
|
||||
|
||||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {Label} from 'src/api'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
children: (labels: Label[]) => JSX.Element
|
||||
}
|
||||
|
||||
interface State {
|
||||
labels: Label[]
|
||||
ready: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FetchLabels extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
labels: [],
|
||||
ready: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const labels = await getLabels()
|
||||
this.setState({ready: RemoteDataState.Done, labels})
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.ready === RemoteDataState.Error) {
|
||||
return (
|
||||
<EmptyState>
|
||||
<EmptyState.Text text="Could not load labels" />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
||||
if (this.state.ready !== RemoteDataState.Done) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return this.props.children(this.state.labels)
|
||||
}
|
||||
}
|
||||
|
||||
export default FetchLabels
|
|
@ -546,6 +546,16 @@ export const cellDeleted = (): Notification => ({
|
|||
message: `Cell deleted from dashboard.`,
|
||||
})
|
||||
|
||||
export const addDashboardLabelFailed = (): Notification => ({
|
||||
...defaultErrorNotification,
|
||||
message: 'Failed to add label to dashboard',
|
||||
})
|
||||
|
||||
export const removedDashboardLabelFailed = (): Notification => ({
|
||||
...defaultErrorNotification,
|
||||
message: 'Failed to remove label from dashboard',
|
||||
})
|
||||
|
||||
export const builderDisabled = (): Notification => ({
|
||||
type: 'info',
|
||||
icon: 'graphline',
|
||||
|
|
|
@ -88,7 +88,7 @@ $ix-text-base-5: (ceil($ix-text-base * $ix-text-scale * $ix-text-scale * $ix-tex
|
|||
|
||||
$form-xs-height: 22px;
|
||||
$form-xs-padding: 6px;
|
||||
$form-xs-font: 13px;
|
||||
$form-xs-font: 12px;
|
||||
|
||||
$form-sm-height: 30px;
|
||||
$form-sm-padding: 9px;
|
||||
|
|
Loading…
Reference in New Issue