diff --git a/http/cur_swagger.yml b/http/cur_swagger.yml index 0393fd927f..ea4ed84077 100644 --- a/http/cur_swagger.yml +++ b/http/cur_swagger.yml @@ -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: diff --git a/ui/src/api/api.ts b/ui/src/api/api.ts index d1c31a633a..0386cd58bb 100644 --- a/ui/src/api/api.ts +++ b/ui/src/api/api.ts @@ -1110,10 +1110,10 @@ export interface InlineResponse200 { export interface InlineResponse2001 { /** * - * @type {Array} + * @type {Label} * @memberof InlineResponse2001 */ - tasks?: Array; + label?: Label; /** * * @type {Links} @@ -1130,14 +1130,54 @@ export interface InlineResponse2001 { export interface InlineResponse2002 { /** * - * @type {Array} + * @type {Label} * @memberof InlineResponse2002 */ + labels?: Label; + /** + * + * @type {Links} + * @memberof InlineResponse2002 + */ + links?: Links; +} + +/** + * + * @export + * @interface InlineResponse2003 + */ +export interface InlineResponse2003 { + /** + * + * @type {Array} + * @memberof InlineResponse2003 + */ + tasks?: Array; + /** + * + * @type {Links} + * @memberof InlineResponse2003 + */ + links?: Links; +} + +/** + * + * @export + * @interface InlineResponse2004 + */ +export interface InlineResponse2004 { + /** + * + * @type {Array} + * @memberof InlineResponse2004 + */ runs?: Array; /** * * @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 { + 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 { + dashboardsDashboardIDLabelsPost(dashboardID: string, label: Label, zapTraceSpan?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 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 { + tasksGet(after?: string, user?: string, org?: string, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 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 { + tasksTaskIDRunsGet(taskID: string, after?: string, limit?: number, afterTime?: Date, beforeTime?: Date, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 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}) diff --git a/ui/src/clockface/components/index_views/IndexList.scss b/ui/src/clockface/components/index_views/IndexList.scss index 3f26ef6073..fbe9409844 100644 --- a/ui/src/clockface/components/index_views/IndexList.scss +++ b/ui/src/clockface/components/index_views/IndexList.scss @@ -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; +} diff --git a/ui/src/clockface/components/label/Label.scss b/ui/src/clockface/components/label/Label.scss index b270590ba1..3706a47294 100644 --- a/ui/src/clockface/components/label/Label.scss +++ b/ui/src/clockface/components/label/Label.scss @@ -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; -} - diff --git a/ui/src/clockface/components/label/LabelContainer.scss b/ui/src/clockface/components/label/LabelContainer.scss new file mode 100644 index 0000000000..245f1aebe0 --- /dev/null +++ b/ui/src/clockface/components/label/LabelContainer.scss @@ -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; + } diff --git a/ui/src/clockface/components/label/LabelContainer.tsx b/ui/src/clockface/components/label/LabelContainer.tsx index 53f96e9e19..dc34d87df9 100644 --- a/ui/src/clockface/components/label/LabelContainer.tsx +++ b/ui/src/clockface/components/label/LabelContainer.tsx @@ -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 { public static defaultProps: Partial = { limitChildCount: 999, + resourceName: 'this resource', } public render() { @@ -28,19 +37,39 @@ class LabelContainer extends Component {
{this.children} {this.additionalChildrenIndicator} + {this.editButton}
) } - 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 { return (
+{additionalCount} more - + +
+ ) + } + } + + 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 ( +
+
) } diff --git a/ui/src/clockface/components/label/LabelSelector.tsx b/ui/src/clockface/components/label/LabelSelector.tsx index 2dbb11286f..60f3c630a1 100644 --- a/ui/src/clockface/components/label/LabelSelector.tsx +++ b/ui/src/clockface/components/label/LabelSelector.tsx @@ -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 { } public render() { - const {resourceType} = this.props + const {resourceType, inputSize} = this.props const {filterValue} = this.state return ( @@ -77,6 +78,7 @@ class LabelSelector extends Component { onFocus={this.handleStartSuggesting} onKeyDown={this.handleKeyDown} onChange={this.handleInputChange} + size={inputSize} /> {this.suggestionMenu} @@ -97,12 +99,12 @@ class LabelSelector extends Component { {selectedLabels.map(label => ( @@ -138,7 +140,7 @@ class LabelSelector extends Component { 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 { }) } - 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 { }) 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 { 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 { const highlightedIndex = _.findIndex( availableLabels, - label => label.id === highlightedID + label => label.name === highlightedID ) const adjacentIndex = Math.min( @@ -241,7 +243,7 @@ class LabelSelector extends Component { availableLabels.length - 1 ) - const adjacentID = availableLabels[adjacentIndex].id + const adjacentID = availableLabels[adjacentIndex].name this.setState({highlightedID: adjacentID}) } diff --git a/ui/src/clockface/components/label/LabelSelectorMenu.tsx b/ui/src/clockface/components/label/LabelSelectorMenu.tsx index cae5ad6689..64ad661de1 100644 --- a/ui/src/clockface/components/label/LabelSelectorMenu.tsx +++ b/ui/src/clockface/components/label/LabelSelectorMenu.tsx @@ -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 { if (filteredLabels.length) { return filteredLabels.map(label => ( diff --git a/ui/src/dashboards/actions/v2/index.ts b/ui/src/dashboards/actions/v2/index.ts index 5e44f97bee..2db30d9de1 100644 --- a/ui/src/dashboards/actions/v2/index.ts +++ b/ui/src/dashboards/actions/v2/index.ts @@ -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) => { + 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) => { + try { + await removeDashboardLabelsAJAX(dashboardID, labels) + dispatch(removeDashboardLabels(dashboardID, labels)) + } catch (error) { + console.error(error) + dispatch(notify(copy.removedDashboardLabelFailed())) + } +} diff --git a/ui/src/dashboards/apis/v2/index.ts b/ui/src/dashboards/apis/v2/index.ts index 929ef50e2d..755c8d621d 100644 --- a/ui/src/dashboards/apis/v2/index.ts +++ b/ui/src/dashboards/apis/v2/index.ts @@ -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 => { await cellsAPI.dashboardsDashboardIDCellsCellIDDelete(dashboardID, cell.id) } + +export const addDashboardLabels = async ( + dashboardID: string, + labels: Label[] +): Promise => { + 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 => { + await Promise.all( + labels.map(async label => { + const {data} = await dashboardsAPI.dashboardsDashboardIDLabelsNameDelete( + dashboardID, + label.name + ) + return data + }) + ) +} diff --git a/ui/src/dashboards/components/EditDashboardLabelsOverlay.tsx b/ui/src/dashboards/components/EditDashboardLabelsOverlay.tsx new file mode 100644 index 0000000000..c71b49dfaf --- /dev/null +++ b/ui/src/dashboards/components/EditDashboardLabelsOverlay.tsx @@ -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 { + 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 ( + + + +
+ + + + + + + {labels => ( + + )} + + + + +