From 8620909006d189fae414d43b4c7d98397b34071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Jan 2023 17:08:23 +0100 Subject: [PATCH] Table: Fixes broken link styles after recent cell options PR (#61582) * Table: Fixes broken link styles after recent cell options PR * Share migration and fix bar gauge as well * Remove unused import * Review fixes * Fixed test Co-authored-by: Kyle Cunningham --- .betterer.results | 3 +- .../grafana-schema/src/schema/mudball.cue | 30 +++++-- .../grafana-schema/src/schema/mudball.gen.ts | 35 +++++++-- .../src/components/Table/BarGaugeCell.tsx | 26 +------ .../src/components/Table/DefaultCell.tsx | 51 ++++-------- .../grafana-ui/src/components/Table/utils.tsx | 68 ++++++++++++++++ .../dashboard/state/DashboardMigrator.ts | 56 ++----------- .../panel/table/TableCellOptionEditor.tsx | 78 ++++++------------- 8 files changed, 168 insertions(+), 179 deletions(-) diff --git a/.betterer.results b/.betterer.results index 7c76001a715..f5efc89e878 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1488,7 +1488,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "14"], + [0, 0, 0, "Do not use any type assertions.", "15"] ], "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/packages/grafana-schema/src/schema/mudball.cue b/packages/grafana-schema/src/schema/mudball.cue index ee7aaff82cd..c1098d0204e 100644 --- a/packages/grafana-schema/src/schema/mudball.cue +++ b/packages/grafana-schema/src/schema/mudball.cue @@ -206,7 +206,6 @@ FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(kind="type") // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge") -// : TableCellDisplayMode @cuetsy(kind="enum") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -260,26 +259,41 @@ VizLegendOptions: { // for the bar gauge component of Grafana UI BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum") -// Interface for table cell types that have no additional options. +// Auto mode table cell options TableAutoCellOptions: { - type: TableCellDisplayMode + type: TableCellDisplayMode & "auto" } @cuetsy(kind="interface") -// Allows for the table cell gauge display type to set the gauge mode. +// Colored text cell options +TableColorTextCellOptions: { + type: TableCellDisplayMode & "color-text" +} @cuetsy(kind="interface") + +// Json view cell options +TableJsonViewCellOptions: { + type: TableCellDisplayMode & "json-view" +} @cuetsy(kind="interface") + +// Json view cell options +TableImageCellOptions: { + type: TableCellDisplayMode & "image" +} @cuetsy(kind="interface") + +// Gauge cell options TableBarGaugeCellOptions: { type: TableCellDisplayMode & "gauge" - mode: BarGaugeDisplayMode + mode?: BarGaugeDisplayMode } @cuetsy(kind="interface") -// Allows for the background display mode to be set for the color background cell. +// Colored background cell options TableColoredBackgroundCellOptions: { type: TableCellDisplayMode & "color-background" - mode: TableCellBackgroundDisplayMode + mode?: TableCellBackgroundDisplayMode } @cuetsy(kind="interface") // Table cell options. Each cell has a display mode // and other potential options for that display. -TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index a279dae6967..3c2b02ed86b 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -489,25 +489,46 @@ export enum BarGaugeDisplayMode { } /** - * Interface for table cell types that have no additional options. + * Auto mode table cell options */ export interface TableAutoCellOptions { - type: TableCellDisplayMode; + type: TableCellDisplayMode.Auto; } /** - * Allows for the table cell gauge display type to set the gauge mode. + * Colored text cell options + */ +export interface TableColorTextCellOptions { + type: TableCellDisplayMode.ColorText; +} + +/** + * Json view cell options + */ +export interface TableJsonViewCellOptions { + type: TableCellDisplayMode.JSONView; +} + +/** + * Json view cell options + */ +export interface TableImageCellOptions { + type: TableCellDisplayMode.Image; +} + +/** + * Gauge cell options */ export interface TableBarGaugeCellOptions { - mode: BarGaugeDisplayMode; + mode?: BarGaugeDisplayMode; type: TableCellDisplayMode.Gauge; } /** - * Allows for the background display mode to be set for the color background cell. + * Colored background cell options */ export interface TableColoredBackgroundCellOptions { - mode: TableCellBackgroundDisplayMode; + mode?: TableCellBackgroundDisplayMode; type: TableCellDisplayMode.ColorBackground; } @@ -515,7 +536,7 @@ export interface TableColoredBackgroundCellOptions { * Table cell options. Each cell has a display mode * and other potential options for that display. */ -export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions); +export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions); /** * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index cdb831d81ab..3bbb3b2761a 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -8,6 +8,7 @@ import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; import { TableCellProps, TableCellDisplayMode } from './types'; +import { getCellOptions } from './utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -39,28 +40,9 @@ export const BarGaugeCell: FC = (props) => { // Set default display mode let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; - // Support deprecated settings - const usingDeprecatedSettings = field.config.custom.displayMode !== undefined; - - // If we're using the old settings format we read the displayMode directly from - // the cell options - if (usingDeprecatedSettings) { - if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Lcd) - ) { - barGaugeMode = BarGaugeDisplayMode.Lcd; - } else if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Basic) - ) { - barGaugeMode = BarGaugeDisplayMode.Basic; - } - } - // Otherwise in the case of sub-options we read specifically from the sub-options - // object in order to get the display mode - else { - barGaugeMode = field.config.custom.cellOptions.mode; + const cellOptions = getCellOptions(field); + if (cellOptions.type === TableCellDisplayMode.Gauge) { + barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; } const getLinks = () => { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 1e2c0bcb953..026cfb16e8a 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -2,8 +2,8 @@ import { cx } from '@emotion/css'; import React, { FC, ReactElement } from 'react'; import tinycolor from 'tinycolor2'; -import { DisplayValue, Field, formattedValueToString } from '@grafana/data'; -import { TableCellBackgroundDisplayMode } from '@grafana/schema'; +import { DisplayValue, formattedValueToString } from '@grafana/data'; +import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; @@ -11,6 +11,7 @@ import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; import { TableStyles } from './styles'; import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; +import { getCellOptions } from './utils'; export const DefaultCell: FC = (props) => { const { field, cell, tableStyles, row, cellProps } = props; @@ -27,7 +28,8 @@ export const DefaultCell: FC = (props) => { const showFilters = field.config.filterable; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; - const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled); + const cellOptions = getCellOptions(field); + const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const hasLinks = Boolean(getCellLinks(field, row)?.length); return ( @@ -38,7 +40,7 @@ export const DefaultCell: FC = (props) => { getCellLinks(field, row) || []}> {(api) => { return ( -
+
{value}
); @@ -53,49 +55,24 @@ export const DefaultCell: FC = (props) => { function getCellStyle( tableStyles: TableStyles, - field: Field, + cellOptions: TableCellOptions, displayValue: DisplayValue, disableOverflowOnHover = false ) { // How much to darken elements depends upon if we're in dark mode const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; - // See if we're using deprecated settings - const usingDeprecatedSettings = field.config.custom?.displayMode !== undefined; - // Setup color variables let textColor: string | undefined = undefined; let bgColor: string | undefined = undefined; - // Set colors using deprecated settings format - if (usingDeprecatedSettings) { - if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) { + if (cellOptions.type === TableCellDisplayMode.ColorText) { + textColor = displayValue.color; + } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { + if (cellOptions.mode === TableCellBackgroundDisplayMode.Basic) { textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); bgColor = tinycolor(displayValue.color).toRgbString(); - } else if ( - field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground && - field.config.custom?.backgroundDisplayMode === TableCellBackgroundDisplayMode.Gradient - ) { - const bgColor2 = tinycolor(displayValue.color) - .darken(10 * darkeningFactor) - .spin(5); - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; - } - } - // Set colors using updated sub-options format - else { - const cellDisplayMode = field.config.custom?.cellOptions?.mode; - const cellDisplayType = field.config.custom?.cellOptions?.type; - - if (cellDisplayType === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Basic) { - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = tinycolor(displayValue.color).toRgbString(); - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Gradient) { + } else if (cellOptions.mode === TableCellBackgroundDisplayMode.Gradient) { const bgColor2 = tinycolor(displayValue.color) .darken(10 * darkeningFactor) .spin(5); @@ -113,8 +90,8 @@ function getCellStyle( return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer; } -function getLinkStyle(tableStyles: TableStyles, field: Field, targetClassName: string | undefined) { - if (field.config.custom?.displayMode === TableCellDisplayMode.Auto) { +function getLinkStyle(tableStyles: TableStyles, cellOptions: TableCellOptions, targetClassName: string | undefined) { + if (cellOptions.type === TableCellDisplayMode.Auto) { return cx(tableStyles.cellLink, targetClassName); } diff --git a/packages/grafana-ui/src/components/Table/utils.tsx b/packages/grafana-ui/src/components/Table/utils.tsx index edaa526e0b1..77b68af4b1b 100644 --- a/packages/grafana-ui/src/components/Table/utils.tsx +++ b/packages/grafana-ui/src/components/Table/utils.tsx @@ -16,6 +16,12 @@ import { GrafanaTheme2, ArrayVector, } from '@grafana/data'; +import { + BarGaugeDisplayMode, + TableAutoCellOptions, + TableCellBackgroundDisplayMode, + TableCellOptions, +} from '@grafana/schema'; import { BarGaugeCell } from './BarGaugeCell'; import { DefaultCell } from './DefaultCell'; @@ -344,3 +350,65 @@ export function createFooterCalculationValues(rows: Row[]): any[number] { return values; } + +const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto }; + +export function getCellOptions(field: Field): TableCellOptions { + if (field.config.custom?.displayMode) { + return migrateTableDisplayModeToCellOptions(field.config.custom?.displayMode); + } + + if (!field.config.custom?.cellOptions) { + return defaultCellOptions; + } + + return (field.config.custom as TableFieldOptions).cellOptions; +} + +/** + * Migrates table cell display mode to new object format. + * + * @param displayMode The display mode of the cell + * @returns TableCellOptions object in the correct format + * relative to the old display mode. + */ +export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDisplayMode): TableCellOptions { + switch (displayMode) { + // In the case of the gauge we move to a different option + case 'basic': + case 'gradient-gauge': + case 'lcd-gauge': + let gaugeMode = BarGaugeDisplayMode.Basic; + + if (displayMode === 'gradient-gauge') { + gaugeMode = BarGaugeDisplayMode.Gradient; + } else if (displayMode === 'lcd-gauge') { + gaugeMode = BarGaugeDisplayMode.Lcd; + } + + return { + type: TableCellDisplayMode.Gauge, + mode: gaugeMode, + }; + // Also true in the case of the color background + case 'color-background': + case 'color-background-solid': + let mode = TableCellBackgroundDisplayMode.Basic; + + // Set the new mode field, somewhat confusingly the + // color-background mode is for gradient display + if (displayMode === 'color-background') { + mode = TableCellBackgroundDisplayMode.Gradient; + } + + return { + type: TableCellDisplayMode.ColorBackground, + mode: mode, + }; + default: + return { + // @ts-ignore + type: displayMode, + }; + } +} diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index fb6073d83e7..51deeecd53c 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -26,8 +26,8 @@ import { import { labelsToFieldsTransformer } from '@grafana/data/src/transformations/transformers/labelsToFields'; import { mergeTransformer } from '@grafana/data/src/transformations/transformers/merge'; import { getDataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; -import { BarGaugeDisplayMode, TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; -import { AxisPlacement, GraphFieldConfig, TableCellDisplayMode } from '@grafana/ui'; +import { AxisPlacement, GraphFieldConfig } from '@grafana/ui'; +import { migrateTableDisplayModeToCellOptions } from '@grafana/ui/src/components/Table/utils'; import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/OptionsUI/registry'; import { config } from 'app/core/config'; import { @@ -818,7 +818,7 @@ export class DashboardMigrator { // Update field configuration if (displayMode !== undefined) { // Migrate any options for the panel - panel.fieldConfig.defaults.custom.cellOptions = migrateTableCellConfig(displayMode); + panel.fieldConfig.defaults.custom.cellOptions = migrateTableDisplayModeToCellOptions(displayMode); // Delete the legacy field delete panel.fieldConfig.defaults.custom.displayMode; @@ -831,7 +831,8 @@ export class DashboardMigrator { if (panel.fieldConfig.overrides[i].properties[j].id === 'custom.displayMode') { panel.fieldConfig.overrides[i].properties[j].id = 'custom.cellOptions'; - panel.fieldConfig.overrides[i].properties[j].value = migrateTableCellConfig(overrideDisplayMode); + panel.fieldConfig.overrides[i].properties[j].value = + migrateTableDisplayModeToCellOptions(overrideDisplayMode); } } } @@ -1364,50 +1365,3 @@ function ensureXAxisVisibility(panel: PanelModel) { return panel; } - -/** - * Migrates table cell display mode to new object format. - * - * @param displayMode The display mode of the cell - * @returns TableCellOptions object in the correct format - * relative to the old display mode. - */ -function migrateTableCellConfig(displayMode: TableCellDisplayMode): TableCellOptions { - switch (displayMode) { - // In the case of the gauge we move to a different option - case 'basic': - case 'gradient-gauge': - case 'lcd-gauge': - let gaugeMode = BarGaugeDisplayMode.Basic; - - if (displayMode === 'gradient-gauge') { - gaugeMode = BarGaugeDisplayMode.Gradient; - } else if (displayMode === 'lcd-gauge') { - gaugeMode = BarGaugeDisplayMode.Lcd; - } - - return { - type: TableCellDisplayMode.Gauge, - mode: gaugeMode, - }; - // Also true in the case of the color background - case 'color-background': - case 'color-background-solid': - let mode = TableCellBackgroundDisplayMode.Basic; - - // Set the new mode field, somewhat confusingly the - // color-background mode is for gradient display - if (displayMode === 'color-background') { - mode = TableCellBackgroundDisplayMode.Gradient; - } - - return { - type: TableCellDisplayMode.ColorBackground, - mode: mode, - }; - default: - return { - type: displayMode, - }; - } -} diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index 02b21973316..c1bf6854791 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -1,22 +1,13 @@ import { merge } from 'lodash'; -import React, { ReactNode, useState } from 'react'; +import React, { useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { TableCellOptions } from '@grafana/schema'; -import { Field, HorizontalGroup, Select, TableCellDisplayMode } from '@grafana/ui'; +import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; -const cellDisplayModeOptions = [ - { value: TableCellDisplayMode.Auto, label: 'Auto' }, - { value: TableCellDisplayMode.ColorText, label: 'Colored text' }, - { value: TableCellDisplayMode.ColorBackground, label: 'Colored background' }, - { value: TableCellDisplayMode.Gauge, label: 'Gauge' }, - { value: TableCellDisplayMode.JSONView, label: 'JSON View' }, - { value: TableCellDisplayMode.Image, label: 'Image' }, -]; - // The props that any cell type editor are expected // to handle. In this case the generic type should // be a discriminated interface of TableCellOptions @@ -25,29 +16,6 @@ export interface TableCellEditorProps { onChange: (value: T) => void; } -// Maps display modes to editor components -interface ComponentMap { - [key: string]: Function; -} - -// Maps cell type to options for caching -interface SettingMap { - [key: string]: TableCellOptions; -} - -/* - Map of display modes to editor components - Additional cell types can be placed here - --- - A cell editor is expected to be a functional - component that accepts options and displays - them in a form. -*/ -const displayModeComponentMap: ComponentMap = { - [TableCellDisplayMode.Gauge]: BarGaugeCellOptionsEditor, - [TableCellDisplayMode.ColorBackground]: ColorBackgroundCellOptionsEditor, -}; - interface Props { value: TableCellOptions; onChange: (v: TableCellOptions) => void; @@ -55,25 +23,21 @@ interface Props { export const TableCellOptionEditor = ({ value, onChange }: Props) => { const cellType = value.type; - let editor: ReactNode | null = null; - let [settingCache, setSettingCache] = useState({}); + const currentMode = cellDisplayModeOptions.find((o) => o.value!.type === cellType)!; + + let [settingCache, setSettingCache] = useState>({}); // Update display mode on change - const onCellTypeChange = (v: SelectableValue) => { + const onCellTypeChange = (v: SelectableValue) => { if (v.value !== undefined) { // Set the new type of cell starting // with default settings - value = { - type: v.value, - }; + value = v.value; // When changing cell type see if there were previously stored // settings and merge those with the changed value - if (settingCache[v.value] !== undefined && Object.keys(settingCache[v.value]).length > 1) { - settingCache[v.value] = merge(value, settingCache[v.value]); - setSettingCache(settingCache); - onChange(settingCache[v.value]); - return; + if (settingCache[value.type] !== undefined && Object.keys(settingCache[value.type]).length > 1) { + value = merge(value, settingCache[value.type]); } onChange(value); @@ -88,19 +52,27 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { onChange(settingCache[value.type]); }; - // Setup specific cell editor - if (cellType !== undefined && displayModeComponentMap[cellType] !== undefined) { - let Comp: Function = displayModeComponentMap[cellType]; - editor = ; - } - // Setup and inject editor return ( <> - - {editor} + {cellType === TableCellDisplayMode.Gauge && ( + + )} + {cellType === TableCellDisplayMode.ColorBackground && ( + + )} ); }; + +const cellDisplayModeOptions: Array> = [ + { value: { type: TableCellDisplayMode.Auto }, label: 'Auto' }, + { value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' }, + { value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' }, + { value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' }, + { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, + { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, +];