diff --git a/src/components/CardEditor/CardEditForm/CardEditForm.jsx b/src/components/CardEditor/CardEditForm/CardEditForm.jsx index 86fde796cd..039c2f0514 100644 --- a/src/components/CardEditor/CardEditForm/CardEditForm.jsx +++ b/src/components/CardEditor/CardEditForm/CardEditForm.jsx @@ -57,6 +57,10 @@ const propTypes = { * this prop will be ignored if getValidDataItems is defined */ dataItems: DataItemsPropTypes, + /** an object where the keys are available dimensions and the values are the values available for those dimensions + * ex: { manufacturer: ['Rentech', 'GHI Industries'], deviceid: ['73000', '73001', '73002'] } + */ + availableDimensions: PropTypes.shape({}), /** If provided, runs the function when the user clicks submit in the Card code JSON editor * onValidateCardJson(cardConfig) * @returns Array error strings. return empty array if there is no errors @@ -90,6 +94,7 @@ const defaultProps = { getValidDataItems: null, getValidTimeRanges: null, dataItems: [], + availableDimensions: {}, onValidateCardJson: null, }; @@ -166,6 +171,7 @@ const CardEditForm = ({ onValidateCardJson, getValidDataItems, getValidTimeRanges, + availableDimensions, }) => { const mergedI18n = { ...defaultProps.i18n, ...i18n }; const [showEditor, setShowEditor] = useState(false); @@ -206,6 +212,7 @@ const CardEditForm = ({ onChange={onChange} i18n={mergedI18n} dataItems={dataItems} + availableDimensions={availableDimensions} getValidDataItems={getValidDataItems} getValidTimeRanges={getValidTimeRanges} /> diff --git a/src/components/CardEditor/CardEditForm/CardEditFormContent.jsx b/src/components/CardEditor/CardEditForm/CardEditFormContent.jsx index b165893834..728a41cf9c 100644 --- a/src/components/CardEditor/CardEditForm/CardEditFormContent.jsx +++ b/src/components/CardEditor/CardEditForm/CardEditFormContent.jsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { - CARD_TYPES, CARD_SIZES, CARD_DIMENSIONS, ALLOWED_CARD_SIZES_PER_TYPE, @@ -90,6 +89,10 @@ const propTypes = { * this prop will be ignored if getValidDataItems is defined */ dataItems: DataItemsPropTypes, + /** an object where the keys are available dimensions and the values are the values available for those dimensions + * ex: { manufacturer: ['Rentech', 'GHI Industries'], deviceid: ['73000', '73001', '73002'] } + */ + availableDimensions: PropTypes.shape({}), }; const defaultProps = { @@ -130,6 +133,7 @@ const defaultProps = { getValidDataItems: null, getValidTimeRanges: null, dataItems: [], + availableDimensions: {}, }; const defaultTimeRangeOptions = [ @@ -163,6 +167,7 @@ const CardEditFormContent = ({ dataItems, getValidDataItems, getValidTimeRanges, + availableDimensions, }) => { const { title, description, size, type, id } = cardConfig; const mergedI18n = { ...defaultProps.i18n, ...i18n }; @@ -220,46 +225,43 @@ const CardEditFormContent = ({ titleText={mergedI18n.size} /> - {type === CARD_TYPES.TIMESERIES && ( - <> -
- item.text} - items={ - validTimeRanges - ? validTimeRanges.map((range) => ({ - id: range, - text: mergedI18n[range] || range, - })) - : [] - } - light - onChange={({ selectedItem }) => { - const { range, interval } = timeRangeToJSON[selectedItem.id]; - setSelectedTimeRange(selectedItem.id); - onChange({ - ...cardConfig, - interval, - dataSource: { ...cardConfig.dataSource, range }, - }); - }} - titleText={mergedI18n.timeRange} - /> -
- - - )} +
+ item.text} + items={ + validTimeRanges + ? validTimeRanges.map((range) => ({ + id: range, + text: mergedI18n[range] || range, + })) + : [] + } + light + onChange={({ selectedItem }) => { + const { range, interval } = timeRangeToJSON[selectedItem.id]; + setSelectedTimeRange(selectedItem.id); + onChange({ + ...cardConfig, + interval, + dataSource: { ...cardConfig.dataSource, range }, + }); + }} + titleText={mergedI18n.timeRange} + /> +
+ ); }; diff --git a/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItem.jsx b/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItem.jsx index 9c5596e7d6..68111ee628 100644 --- a/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItem.jsx +++ b/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItem.jsx @@ -1,31 +1,17 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Edit16 } from '@carbon/icons-react'; -import { - purple70, - cyan50, - teal70, - magenta70, - red50, - red90, - green60, - blue80, - magenta50, - purple50, - teal50, - cyan90, -} from '@carbon/colors'; -import classnames from 'classnames'; import { settings } from '../../../../constants/Settings'; -import { - ComposedModal, - Button, - List, - TextInput, - MultiSelect, -} from '../../../../index'; +import { Button, List, MultiSelect } from '../../../../index'; import { DataItemsPropTypes } from '../../../DashboardEditor/DashboardEditor'; +import { + DATAITEM_COLORS_OPTIONS, + handleDataSeriesChange, +} from '../../../DashboardEditor/editorUtils'; +import { CARD_TYPES } from '../../../../constants/LayoutConstants'; + +import DataSeriesFormItemModal from './DataSeriesFormItemModal'; const { iotPrefix } = settings; @@ -66,6 +52,10 @@ const propTypes = { * this prop will be ignored if getValidDataItems is defined */ dataItems: DataItemsPropTypes, + /** an object where the keys are available dimensions and the values are the values available for those dimensions + * ex: { manufacturer: ['Rentech', 'GHI Industries'], deviceid: ['73000', '73001', '73002'] } + */ + availableDimensions: PropTypes.shape({}), setSelectedDataItems: PropTypes.func.isRequired, selectedTimeRange: PropTypes.string.isRequired, }; @@ -84,44 +74,7 @@ const defaultProps = { }, getValidDataItems: null, dataItems: [], -}; - -const DATAITEM_COLORS_OPTIONS = [ - purple70, - cyan50, - teal70, - magenta70, - red50, - red90, - green60, - blue80, - magenta50, - purple50, - teal50, - cyan90, -]; - -/** - * returns a new series array with a generated color if needed, and in the format expected by the JSON payload - * @param {array} selectedItems - * @param {object} cardConfig - */ -export const formatSeries = (selectedItems, cardJson) => { - const cardSeries = cardJson?.content?.series; - const series = selectedItems.map(({ id }, i) => { - const currentItem = cardSeries?.find( - (dataItem) => dataItem.dataSourceId === id - ); - const color = - currentItem?.color ?? - DATAITEM_COLORS_OPTIONS[i % DATAITEM_COLORS_OPTIONS.length]; - return { - dataSourceId: id, - label: currentItem?.label || id, - color, - }; - }); - return series; + availableDimensions: {}, }; export const formatDataItemsForDropdown = (dataItems) => @@ -137,6 +90,7 @@ const DataSeriesFormItem = ({ onChange, setSelectedDataItems, selectedTimeRange, + availableDimensions, i18n, }) => { const mergedI18n = { ...defaultProps.i18n, ...i18n }; @@ -146,9 +100,13 @@ const DataSeriesFormItem = ({ const baseClassName = `${iotPrefix}--card-edit-form`; - const initialSelectedItems = formatDataItemsForDropdown( - cardConfig?.content?.series - ); + // determine which content section to look at + const dataSection = + cardConfig.type === CARD_TYPES.TIMESERIES + ? cardConfig?.content?.series + : cardConfig?.content?.attributes; + + const initialSelectedItems = formatDataItemsForDropdown(dataSection); const validDataItems = getValidDataItems ? getValidDataItems(cardConfig, selectedTimeRange) @@ -156,69 +114,16 @@ const DataSeriesFormItem = ({ return ( <> - {showEditor ? ( - { - const updatedSeries = [...cardConfig.content.series]; - const editDataItemIndex = updatedSeries.findIndex( - (dataItem) => dataItem.dataSourceId === editDataItem.dataSourceId - ); - updatedSeries[editDataItemIndex] = editDataItem; - onChange({ - ...cardConfig, - content: { ...cardConfig.content, series: updatedSeries }, - }); - setShowEditor(false); - setEditDataItem(null); - }} - onClose={() => { - setShowEditor(false); - setEditDataItem(null); - }}> - - {mergedI18n.dataItemEditorDataItemTitle} - -
- {editDataItem.dataSourceId} -
-
- - setEditDataItem({ - ...editDataItem, - label: evt.target.value, - }) - } - value={editDataItem.label} - /> -
-
- - {mergedI18n.dataItemEditorLegendColor} - -
- {DATAITEM_COLORS_OPTIONS.map((color) => ( -
-
-
- ) : null} +
{mergedI18n.dataSeriesTitle}
@@ -233,12 +138,13 @@ const DataSeriesFormItem = ({ items={formatDataItemsForDropdown(validDataItems)} light onChange={({ selectedItems }) => { - const series = formatSeries(selectedItems, cardConfig); + const newCard = handleDataSeriesChange( + selectedItems, + cardConfig, + onChange + ); setSelectedDataItems(selectedItems.map(({ id }) => id)); - onChange({ - ...cardConfig, - content: { ...cardConfig.content, series }, - }); + onChange(newCard); }} titleText={mergedI18n.dataItem} /> @@ -247,30 +153,33 @@ const DataSeriesFormItem = ({ // need to force an empty "empty state" emptyState={
} title="" - items={cardConfig?.content?.series?.map((series, i) => ({ - id: series.dataSourceId, + items={dataSection?.map((dataItem, i) => ({ + id: dataItem.dataSourceId, content: { - value: series.label, - icon: ( -
- ), + value: dataItem.label, + icon: + cardConfig.type === CARD_TYPES.TIMESERIES ? ( +
+ ) : null, rowActions: [
+
+ + ); + + const selectedDimensionFilter = editDataItem.dataFilter + ? Object.keys(editDataItem.dataFilter)[0] + : ''; + + const ValueContent = ( + <> +
+
+ + setEditDataItem({ + ...editDataItem, + label: evt.target.value, + }) + } + value={editDataItem.label} + /> +
+
+ + setEditDataItem({ + ...editDataItem, + unit: evt.target.value, + }) + } + value={editDataItem.unit} + /> +
+
+
+
+ { + if (selectedItem !== 'None') { + const dataFilter = { + [selectedItem]: availableDimensions[selectedItem][0], + }; + setEditDataItem({ + ...editDataItem, + dataFilter, + }); + } else { + setEditDataItem({ + ...omit(editDataItem, 'dataFilter'), + }); + } + }} + titleText={mergedI18n.dataItemEditorDataItemFilter} + /> +
+ {!isEmpty(editDataItem.dataFilter) && ( +
+ { + const dataFilter = { [selectedDimensionFilter]: selectedItem }; + setEditDataItem({ + ...editDataItem, + dataFilter, + }); + }} + /> +
+ )} +
+ , name: 'Warning alt' }} + selectedColor={{ carbonColor: red60, name: 'red60' }} + onChange={(thresholds) => { + setEditDataItem({ + ...editDataItem, + thresholds, + }); + }} + /> + + ); + + return ( + <> + {showEditor ? ( +
+ { + const newCard = handleDataItemEdit(editDataItem, cardConfig); + onChange(newCard); + setShowEditor(false); + setEditDataItem({}); + }} + onClose={() => { + setShowEditor(false); + setEditDataItem({}); + }}> + {cardConfig.type === 'TIMESERIES' + ? TimeSeriesContent + : ValueContent} + +
+ ) : null} + + ); +}; +DataSeriesFormItemModal.defaultProps = defaultProps; +DataSeriesFormItemModal.propTypes = propTypes; +export default DataSeriesFormItemModal; diff --git a/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.jsx b/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.jsx new file mode 100644 index 0000000000..e34d0094b6 --- /dev/null +++ b/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.jsx @@ -0,0 +1,280 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Add16, TrashCan32 } from '@carbon/icons-react'; +import omit from 'lodash/omit'; +import isEmpty from 'lodash/isEmpty'; +import uuid from 'uuid'; +import { red60 } from '@carbon/colors'; + +import { settings } from '../../../../constants/Settings'; +import { Button, NumberInput, Dropdown } from '../../../../index'; +import { + validThresholdIcons, + validThresholdColors, +} from '../../../DashboardEditor/editorUtils'; +import SimpleIconDropdown from '../../../SimpleIconDropdown/SimpleIconDropdown'; +import ColorDropdown from '../../../ColorDropdown/ColorDropdown'; + +const { iotPrefix } = settings; + +const propTypes = { + /* card value */ + cardConfig: PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + size: PropTypes.string, + type: PropTypes.string, + content: PropTypes.shape({ + series: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + dataSourceId: PropTypes.string, + color: PropTypes.string, + }) + ), + xLabel: PropTypes.string, + yLabel: PropTypes.string, + unit: PropTypes.string, + includeZeroOnXaxis: PropTypes.bool, + includeZeroOnYaxis: PropTypes.bool, + timeDataSourceId: PropTypes.string, + }), + interval: PropTypes.string, + showLegend: PropTypes.bool, + }), + onChange: PropTypes.func, + thresholds: PropTypes.arrayOf( + PropTypes.shape({ + dataSourceId: PropTypes.string, + comparison: PropTypes.string, + value: PropTypes.number, + color: PropTypes.string, + icon: PropTypes.string, + }) + ), + icons: PropTypes.arrayOf( + PropTypes.shape({ + carbonIcon: PropTypes.any, + name: PropTypes.string, + color: PropTypes.string, + }) + ), + /** default icon for each threshold */ + selectedIcon: PropTypes.shape({ + carbonIcon: PropTypes.any, + name: PropTypes.string, + color: PropTypes.string, + }), + colors: PropTypes.arrayOf( + PropTypes.shape({ + carbonColor: PropTypes.string, + name: PropTypes.string, + }) + ), + /** default color for each threshold */ + selectedColor: PropTypes.shape({ + carbonColor: PropTypes.string, + name: PropTypes.string, + }), + i18n: PropTypes.shape({ + dataItemEditorDataItemThresholds: PropTypes.string, + dataItemEditorDataItemAddThreshold: PropTypes.string, + }), +}; + +const defaultProps = { + cardConfig: {}, + onChange: null, + thresholds: [], + i18n: { + dataItemEditorDataItemThresholds: 'Thresholds', + dataItemEditorDataItemAddThreshold: 'Add threshold', + }, + icons: validThresholdIcons, + selectedIcon: undefined, + colors: validThresholdColors, + selectedColor: undefined, +}; + +const ThresholdsFormItem = ({ + cardConfig, + thresholds: thresholdsProp, + icons, + selectedIcon, + colors, + selectedColor, + onChange, + i18n, +}) => { + const mergedI18n = { ...defaultProps.i18n, ...i18n }; + const baseClassName = `${iotPrefix}--card-edit-form`; + + // initialize thresholds with a unique id + const [thresholds, setThresholds] = useState( + thresholdsProp.map((threshold) => ({ ...threshold, id: uuid.v4() })) + ); + + return ( + <> + {!isEmpty(thresholds) && ( + + {mergedI18n.dataItemEditorDataItemThresholds} + + )} + {thresholds.map((threshold, i) => { + // add threshold color to all icon options + const iconsWithColors = icons.map((icon) => ({ + ...icon, + ...(threshold.color ? { color: threshold.color } : {}), + })); + + // add threshold color to selected icon + const selectedThresholdIconWithColor = { + ...validThresholdIcons.find((icon) => icon.name === threshold.icon), + ...(threshold.color ? { color: threshold.color } : {}), + }; + + // get threshold color to initialize color dropdown + const thresholdColor = colors.find( + (color) => color.carbonColor === threshold.color + ); + + return ( +
+
+
+ { + const updatedThresholds = [...thresholds]; + updatedThresholds[i] = { + ...updatedThresholds[i], + icon: icon.name, + }; + onChange(updatedThresholds.map((item) => omit(item, 'id'))); + setThresholds(updatedThresholds); + }} + /> +
+
+ { + const updatedThresholds = [...thresholds]; + updatedThresholds[i] = { + ...updatedThresholds[i], + color: color.carbonColor, + }; + onChange(updatedThresholds.map((item) => omit(item, 'id'))); + setThresholds(updatedThresholds); + }} + /> +
+
+ ', '<', '=']} // current valid comparison operators + selectedItem={threshold.comparison || '>'} + onChange={({ selectedItem }) => { + const updatedThresholds = [...thresholds]; + updatedThresholds[i] = { + ...updatedThresholds[i], + comparison: selectedItem, + }; + onChange(updatedThresholds.map((item) => omit(item, 'id'))); + setThresholds(updatedThresholds); + }} + /> +
+
+ { + const updatedThresholds = [...thresholds]; + updatedThresholds[i] = { + ...updatedThresholds[i], + value: + Number(imaginaryTarget.value) || imaginaryTarget.value, + }; + onChange(updatedThresholds.map((item) => omit(item, 'id'))); + setThresholds(updatedThresholds); + }} + /> +
+
+
+ ); + })} + + + ); +}; + +ThresholdsFormItem.defaultProps = defaultProps; +ThresholdsFormItem.propTypes = propTypes; +export default ThresholdsFormItem; diff --git a/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.story.jsx b/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.story.jsx new file mode 100644 index 0000000000..b77c4adf6d --- /dev/null +++ b/src/components/CardEditor/CardEditForm/CardEditFormItems/ThresholdsFormItem.story.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { red60, green50, orange40 } from '@carbon/colors'; +import { Checkmark24, MisuseOutline24 } from '@carbon/icons-react'; + +import ThresholdsFormItem from './ThresholdsFormItem'; + +export default { + title: + 'Watson IoT Experimental/CardEditor/CardEditFormItems/ThresholdFormItem', + decorators: [withKnobs], + parameters: { + component: ThresholdsFormItem, + }, + excludeStories: [], +}; + +export const DefaultExample = () => ( +
+ , name: 'Checkmark' }} + selectedColor={{ carbonColor: red60, name: 'red60' }} + onChange={(thresholds) => console.log(thresholds)} + /> +
+); + +export const ExampleWithInitialValues = () => ( +
+ ', value: 0, icon: 'Warning', color: red60 }, + { comparison: '<', value: 5, icon: 'Warning', color: green50 }, + { comparison: '=', value: 20, icon: 'Warning', color: orange40 }, + ]} + selectedIcon={{ carbonIcon: , name: 'Misuse outline' }} + onChange={(thresholds) => console.log(thresholds)} + /> +
+); + +DefaultExample.story = { + parameters: { + info: { + propTables: [ThresholdsFormItem], + propTablesExclude: [], + }, + }, +}; diff --git a/src/components/CardEditor/CardEditForm/CardEditFormItems/__snapshots__/ThresholdsFormItem.story.storyshot b/src/components/CardEditor/CardEditForm/CardEditFormItems/__snapshots__/ThresholdsFormItem.story.storyshot new file mode 100644 index 0000000000..1c24a2d7b5 --- /dev/null +++ b/src/components/CardEditor/CardEditForm/CardEditFormItems/__snapshots__/ThresholdsFormItem.story.storyshot @@ -0,0 +1,1255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/CardEditor/CardEditFormItems/ThresholdFormItem Default Example 1`] = ` +
+
+
+ +
+
+ +
+`; + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/CardEditor/CardEditFormItems/ThresholdFormItem Example With Initial Values 1`] = ` +
+
+
+ + Thresholds + +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+`; diff --git a/src/components/CardEditor/CardEditForm/CardEditFormSettings.jsx b/src/components/CardEditor/CardEditForm/CardEditFormSettings.jsx index 8e02fcacd5..a102152729 100644 --- a/src/components/CardEditor/CardEditForm/CardEditFormSettings.jsx +++ b/src/components/CardEditor/CardEditForm/CardEditFormSettings.jsx @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import omit from 'lodash/omit'; import { settings } from '../../../constants/Settings'; -import { TextInput } from '../../../index'; +import { TextInput, NumberInput, Dropdown } from '../../../index'; +import { CARD_TYPES } from '../../../constants/LayoutConstants'; const { iotPrefix } = settings; @@ -38,7 +40,9 @@ const propTypes = { yAxisLabel: PropTypes.string, unitLabel: PropTypes.string, decimalPrecisionLabel: PropTypes.string, - showLegendLable: PropTypes.string, + precisionLabel: PropTypes.string, + showLegendLabel: PropTypes.string, + fontSize: PropTypes.string, }), }; @@ -49,17 +53,19 @@ const defaultProps = { yAxisLabel: 'Y-axis label', unitLabel: 'Unit', decimalPrecisionLabel: 'Decimal precision', - showLegendLable: 'Show legend', + precisionLabel: 'Precision', + showLegendLabel: 'Show legend', + fontSize: 'Font size', }, }; const CardEditFormSettings = ({ cardConfig, onChange, i18n }) => { const mergedI18n = { ...defaultProps.i18n, ...i18n }; - const { content, id } = cardConfig; + const { content, id, type } = cardConfig; const baseClassName = `${iotPrefix}--card-edit-form`; - return ( + const TimeSeriesSettings = ( <>
{
*/} ); + + const ValueCardSettings = ( + <> +
+ + onChange({ + ...cardConfig, + content: { + ...content, + fontSize: + Number(imaginaryTarget.value) || imaginaryTarget.value, + }, + }) + } + /> +
+
+ { + const isSet = selectedItem !== 'Not set'; + if (isSet) { + onChange({ + ...cardConfig, + content: { + ...content, + precision: Number(selectedItem) || selectedItem, + }, + }); + } else { + onChange({ + ...cardConfig, + content: { + ...omit(content, 'precision'), + }, + }); + } + }} + /> +
+ + ); + + switch (type) { + case CARD_TYPES.TIMESERIES: + return TimeSeriesSettings; + case CARD_TYPES.VALUE: + return ValueCardSettings; + default: + return null; + } }; CardEditFormSettings.propTypes = propTypes; diff --git a/src/components/CardEditor/CardEditForm/_card-edit-form.scss b/src/components/CardEditor/CardEditForm/_card-edit-form.scss index 14826ad8dc..04809f1ff1 100644 --- a/src/components/CardEditor/CardEditForm/_card-edit-form.scss +++ b/src/components/CardEditor/CardEditForm/_card-edit-form.scss @@ -73,6 +73,60 @@ margin-top: 0; } } + &--modal-wrapper { + .#{$prefix}--modal.is-visible .#{$prefix}--modal-container { + overflow: visible; + } + .#{$prefix}--modal-content { + overflow: visible; + } + .#{$prefix}--number, + input[type='number'] { + min-width: unset; + } + } + &--input-group { + display: flex; + flex-direction: row; + align-items: flex-end; + padding-bottom: 1rem; + &--item { + margin-right: 1rem; + width: 100%; + } + &--item-half { + margin-right: 1rem; + width: 50%; + } + &--item-end { + width: 100%; + } + &--item-dropdown { + margin-right: 1rem; + max-width: 5rem; + } + } + &--threshold-input-group { + display: flex; + flex-direction: row; + align-items: flex-end; + &--item { + margin-right: 1rem; + width: 100%; + } + &--item-half { + margin-right: 1rem; + width: 50%; + } + &--item-end { + width: 100%; + } + &--item-dropdown { + margin-right: 1rem; + max-width: 5rem; + } + } + &--footer { padding: $spacing-05; & > button { diff --git a/src/components/CardEditor/CardEditor.jsx b/src/components/CardEditor/CardEditor.jsx index d1af6163a5..ace7db9efe 100644 --- a/src/components/CardEditor/CardEditor.jsx +++ b/src/components/CardEditor/CardEditor.jsx @@ -62,6 +62,10 @@ const propTypes = { label: PropTypes.string, }) ), + /** an object where the keys are available dimensions and the values are the values available for those dimensions + * ex: { manufacturer: ['Rentech', 'GHI Industries'], deviceid: ['73000', '73001', '73002'] } + */ + availableDimensions: PropTypes.shape({}), /** If provided, runs the function when the user clicks submit in the Card code JSON editor * onValidateCardJson(cardConfig) * @returns Array error strings. return empty array if there is no errors @@ -88,6 +92,7 @@ const defaultProps = { getValidDataItems: null, getValidTimeRanges: null, dataItems: [], + availableDimensions: {}, supportedCardTypes: Object.keys(DASHBOARD_EDITOR_CARD_TYPES), onValidateCardJson: null, }; @@ -104,6 +109,7 @@ const CardEditor = ({ dataItems, onValidateCardJson, supportedCardTypes, + availableDimensions, i18n, }) => { const mergedI18n = { ...defaultProps.i18n, ...i18n }; @@ -140,6 +146,7 @@ const CardEditor = ({ getValidDataItems={getValidDataItems} getValidTimeRanges={getValidTimeRanges} onValidateCardJson={onValidateCardJson} + availableDimensions={availableDimensions} i18n={mergedI18n} /> )} diff --git a/src/components/CardEditor/CardEditor.test.jsx b/src/components/CardEditor/CardEditor.test.jsx index 45d7973650..aef19312da 100644 --- a/src/components/CardEditor/CardEditor.test.jsx +++ b/src/components/CardEditor/CardEditor.test.jsx @@ -41,10 +41,7 @@ describe('CardEditor', () => { onAddCard={actions.onAddCard} /> ); - userEvent.type( - screen.getByRole('textbox', { name: 'Card title X-axis label' }), - 'z' - ); + userEvent.type(screen.getByRole('textbox', { name: 'Card title' }), 'z'); userEvent.tab(); expect(actions.onChange).toHaveBeenCalledWith({ ...defaultCard, diff --git a/src/components/CardEditor/__snapshots__/CardEditor.story.storyshot b/src/components/CardEditor/__snapshots__/CardEditor.story.storyshot index dd5c903219..6b23802d82 100644 --- a/src/components/CardEditor/__snapshots__/CardEditor.story.storyshot +++ b/src/components/CardEditor/__snapshots__/CardEditor.story.storyshot @@ -216,8 +216,8 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper > @@ -231,10 +231,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-0-label downshift-0-toggle-button" + aria-labelledby="downshift-9-label downshift-9-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-0-toggle-button" + id="downshift-9-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -269,9 +269,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
-
- +`; + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/ColorDropdown No Labels Example 1`] = ` +
+
+
+
+ +
+ +
@@ -271,10 +404,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-6-label downshift-6-toggle-button" + aria-labelledby="downshift-21-label downshift-21-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-6-toggle-button" + id="downshift-21-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -331,9 +464,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -65,7 +65,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-13-label" + aria-labelledby="downshift-29-label" autoComplete="off" className="bx--text-input" data-testid="combo-box" @@ -208,7 +208,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -243,7 +243,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-8-label" + aria-labelledby="downshift-24-label" autoComplete="off" className="bx--text-input bx--text-input--empty" data-testid="combo-box" @@ -349,7 +349,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -384,7 +384,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-12-label" + aria-labelledby="downshift-28-label" autoComplete="off" className="bx--text-input bx--text-input--empty" data-testid="combo-box" @@ -490,7 +490,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -525,7 +525,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-11-label" + aria-labelledby="downshift-27-label" autoComplete="off" className="bx--text-input bx--text-input--empty" data-testid="combo-box" @@ -635,7 +635,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -670,7 +670,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-9-label" + aria-labelledby="downshift-25-label" autoComplete="off" className="bx--text-input bx--text-input--empty" data-testid="combo-box" @@ -768,7 +768,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
Combobox title @@ -803,7 +803,7 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-activedescendant={null} aria-autocomplete="list" aria-controls={null} - aria-labelledby="downshift-10-label" + aria-labelledby="downshift-26-label" autoComplete="off" className="bx--text-input" data-testid="combo-box" diff --git a/src/components/DashboardEditor/DashboardEditor.jsx b/src/components/DashboardEditor/DashboardEditor.jsx index b28bae0aac..5e95262b4a 100644 --- a/src/components/DashboardEditor/DashboardEditor.jsx +++ b/src/components/DashboardEditor/DashboardEditor.jsx @@ -82,6 +82,10 @@ const propTypes = { * this prop will be ignored if getValidDataItems is defined */ dataItems: DataItemsPropTypes, + /** an object where the keys are available dimensions and the values are the values available for those dimensions + * ex: { manufacturer: ['Rentech', 'GHI Industries'], deviceid: ['73000', '73001', '73002'] } + */ + availableDimensions: PropTypes.shape({}), /** if provided, will update the dashboard json according to its own logic. Can return a valid card to be rendered * onCardChange(updatedCard, template): Card */ @@ -156,6 +160,7 @@ const defaultProps = { getValidDataItems: null, getValidTimeRanges: null, dataItems: [], + availableDimensions: {}, onCardChange: null, onLayoutChange: null, onDelete: null, @@ -220,6 +225,7 @@ const DashboardEditor = ({ onSubmit, submitDisabled, onValidateCardJson, + availableDimensions, isLoading, i18n, }) => { @@ -442,6 +448,7 @@ const DashboardEditor = ({ onAddCard={addCard} onValidateCardJson={onValidateCardJson} supportedCardTypes={supportedCardTypes} + availableDimensions={availableDimensions} i18n={mergedI18n} /> diff --git a/src/components/DashboardEditor/DashboardEditor.story.jsx b/src/components/DashboardEditor/DashboardEditor.story.jsx index 7f04fed8b2..5a36d83574 100644 --- a/src/components/DashboardEditor/DashboardEditor.story.jsx +++ b/src/components/DashboardEditor/DashboardEditor.story.jsx @@ -43,6 +43,10 @@ export const Default = () => ( onSubmit={action('onSubmit')} onLayoutChange={action('onLayoutChange')} submitDisabled={boolean('submitDisabled', false)} + availableDimensions={{ + deviceid: ['73000', '73001', '73002'], + manufacturer: ['rentech', 'GHI Industries'], + }} supportedCardTypes={array('supportedCardTypes', [ 'TIMESERIES', 'SIMPLE_BAR', diff --git a/src/components/DashboardEditor/editorUtils.jsx b/src/components/DashboardEditor/editorUtils.jsx index 1f3ad5af76..60e1575b5c 100644 --- a/src/components/DashboardEditor/editorUtils.jsx +++ b/src/components/DashboardEditor/editorUtils.jsx @@ -2,6 +2,52 @@ import React from 'react'; import uuid from 'uuid'; import isNil from 'lodash/isNil'; import omit from 'lodash/omit'; +import isEmpty from 'lodash/isEmpty'; +import { + purple70, + cyan50, + teal70, + magenta70, + red60, + red50, + orange40, + green60, + blue80, + blue60, + red90, + green50, + yellow30, + magenta50, + purple50, + teal50, + cyan90, +} from '@carbon/colors'; +import { + Checkmark24, + CheckmarkFilled24, + CheckmarkOutline24, + Error24, + ErrorFilled24, + ErrorOutline24, + Help24, + HelpFilled24, + Information24, + InformationFilled24, + Misuse24, + MisuseOutline24, + Undefined24, + UndefinedFilled24, + Unknown24, + UnknownFilled24, + Warning24, + WarningAlt24, + WarningAltFilled24, + WarningAltInverted24, + WarningAltInvertedFilled24, + WarningFilled24, + WarningSquare24, + WarningSquareFilled24, +} from '@carbon/icons-react'; import { CARD_SIZES, @@ -61,7 +107,6 @@ export const getDefaultCard = (type, i18n) => { content: { attributes: [], }, - i18n, }; case DASHBOARD_EDITOR_CARD_TYPES.TIMESERIES: return { @@ -86,7 +131,6 @@ export const getDefaultCard = (type, i18n) => { layout: BAR_CHART_LAYOUTS.VERTICAL, series: [], }, - i18n, }; case DASHBOARD_EDITOR_CARD_TYPES.GROUPED_BAR: return { @@ -96,7 +140,6 @@ export const getDefaultCard = (type, i18n) => { layout: BAR_CHART_LAYOUTS.VERTICAL, series: [], }, - i18n, }; case DASHBOARD_EDITOR_CARD_TYPES.STACKED_BAR: return { @@ -106,7 +149,6 @@ export const getDefaultCard = (type, i18n) => { layout: BAR_CHART_LAYOUTS.VERTICAL, series: [], }, - i18n, }; case DASHBOARD_EDITOR_CARD_TYPES.TABLE: return { @@ -123,7 +165,6 @@ export const getDefaultCard = (type, i18n) => { }, ], }, - i18n, }; case DASHBOARD_EDITOR_CARD_TYPES.IMAGE: return { @@ -135,18 +176,39 @@ export const getDefaultCard = (type, i18n) => { hideHotspots: false, hideZoomControls: false, }, - i18n, }; default: - return { ...baseCardProps, i18n }; + return { ...baseCardProps }; } }; +/** + * Color options for dataItems + */ +export const DATAITEM_COLORS_OPTIONS = [ + purple70, + cyan50, + teal70, + magenta70, + red50, + red90, + green60, + blue80, + magenta50, + purple50, + teal50, + cyan90, +]; + /** * maps a selected time range to what is expected in the dashboardJSON */ export const timeRangeToJSON = { + lastHour: { interval: 'hour', count: -1, type: 'rolling' }, + last2Hours: { interval: 'hour', count: -2, type: 'rolling' }, + last4Hours: { interval: 'hour', count: -4, type: 'rolling' }, + last8Hours: { interval: 'hour', count: -8, type: 'rolling' }, last24Hours: { range: { interval: 'day', count: -1, type: 'rolling' }, interval: 'hour', @@ -209,6 +271,44 @@ export const timeRangeToJSON = { }, }; +export const validThresholdIcons = [ + { carbonIcon: , name: 'Checkmark' }, + { carbonIcon: , name: 'Checkmark filled' }, + { carbonIcon: , name: 'Checkmark outline' }, + { carbonIcon: , name: 'Error' }, + { carbonIcon: , name: 'Error filled' }, + { carbonIcon: , name: 'Error outline' }, + { carbonIcon: , name: 'Help' }, + { carbonIcon: , name: 'Help filled' }, + { carbonIcon: , name: 'Information' }, + { carbonIcon: , name: 'Information filled' }, + { carbonIcon: , name: 'Misuse' }, + { carbonIcon: , name: 'Misuse outline' }, + { carbonIcon: , name: 'Undefined' }, + { carbonIcon: , name: 'Undefined filled' }, + { carbonIcon: , name: 'Unknown' }, + { carbonIcon: , name: 'Unknown filled' }, + { carbonIcon: , name: 'Warning' }, + { carbonIcon: , name: 'Warning alt' }, + { carbonIcon: , name: 'Warning alt filled' }, + { carbonIcon: , name: 'Warning alt inverted' }, + { + carbonIcon: , + name: 'Warning alt inverted filled', + }, + { carbonIcon: , name: 'Warning filled' }, + { carbonIcon: , name: 'Warning square' }, + { carbonIcon: , name: 'Warning square filled' }, +]; + +export const validThresholdColors = [ + { carbonColor: red60, name: 'red60' }, + { carbonColor: green50, name: 'green50' }, + { carbonColor: orange40, name: 'orange40' }, + { carbonColor: yellow30, name: 'yellow30' }, + { carbonColor: blue60, name: 'blue60' }, +]; + /** * determines if a card JSON is valid depending on its card type * @param {Object} cardConfig @@ -247,9 +347,20 @@ const renderDefaultCard = (cardConfig, commonProps) => ( * @returns {Node} */ const renderValueCard = (cardConfig, commonProps) => ( - + { + const iconToRender = validThresholdIcons.find( + (icon) => icon.name === iconName + )?.carbonIcon || ; + // eslint-disable-next-line react/prop-types + return
{iconToRender}
; + }} + isEditable + {...cardConfig} + {...commonProps} + /> ); - /** * @param {Object} cardConfig * @param {Object} commonProps @@ -397,3 +508,119 @@ export const renderBreakpointInfo = (breakpoint, i18n) => { return i18n.layoutInfoXl; } }; + +/** + * returns a new series array with a generated color if needed, and in the format expected by the JSON payload + * @param {array} selectedItems + * @param {object} cardConfig + */ +export const formatSeries = (selectedItems, cardConfig) => { + const cardSeries = cardConfig?.content?.series; + const series = selectedItems.map(({ id }, i) => { + const currentItem = cardSeries?.find( + (dataItem) => dataItem.dataSourceId === id + ); + const color = + currentItem?.color ?? + DATAITEM_COLORS_OPTIONS[i % DATAITEM_COLORS_OPTIONS.length]; + return { + dataSourceId: id, + label: currentItem?.label || id, + color, + }; + }); + return series; +}; + +/** + * returns a new attributes array in the format expected by the JSON payload + * @param {array} selectedItems + * @param {object} cardConfig + */ +export const formatAttributes = (selectedItems, cardConfig) => { + const cardAttributes = cardConfig?.content?.attributes; + const attributes = selectedItems.map(({ id }) => { + const currentItem = cardAttributes?.find( + (dataItem) => dataItem.dataSourceId === id + ); + + return { + dataSourceId: id, + label: currentItem?.label || id, + ...(!isNil(currentItem?.precision) + ? { precision: currentItem.precision } + : {}), + ...(currentItem?.thresholds && !isEmpty(currentItem?.thresholds) + ? { thresholds: currentItem.thresholds } + : {}), + ...(currentItem?.dataFilter + ? { dataFilter: currentItem.dataFilter } + : {}), + }; + }); + return attributes; +}; + +/** + * determines how to format the dataSection based on card type + * @param {array} selectedItems + * @param {object} cardConfig + */ +export const handleDataSeriesChange = (selectedItems, cardConfig) => { + const { type } = cardConfig; + let series; + let attributes; + + switch (type) { + case CARD_TYPES.VALUE: + attributes = formatAttributes(selectedItems, cardConfig); + return { + ...cardConfig, + content: { ...cardConfig.content, attributes }, + }; + case CARD_TYPES.TIMESERIES: + series = formatSeries(selectedItems, cardConfig); + return { + ...cardConfig, + content: { ...cardConfig.content, series }, + }; + default: + return cardConfig; + } +}; + +/** + * updates the dataSection on edit of a dataItem based on card type + * @param {array} editDataItem + * @param {object} cardConfig + */ +export const handleDataItemEdit = (editDataItem, cardConfig) => { + const { type, content } = cardConfig; + let dataSection; + let editDataItemIndex; + + switch (type) { + case CARD_TYPES.VALUE: + dataSection = [...content.attributes]; + editDataItemIndex = dataSection.findIndex( + (dataItem) => dataItem.dataSourceId === editDataItem.dataSourceId + ); + dataSection[editDataItemIndex] = editDataItem; + return { + ...cardConfig, + content: { ...content, attributes: dataSection }, + }; + case CARD_TYPES.TIMESERIES: + dataSection = [...content.series]; + editDataItemIndex = dataSection.findIndex( + (dataItem) => dataItem.dataSourceId === editDataItem.dataSourceId + ); + dataSection[editDataItemIndex] = editDataItem; + return { + ...cardConfig, + content: { ...content, series: dataSection }, + }; + default: + return cardConfig; + } +}; diff --git a/src/components/DashboardEditor/editorUtils.test.jsx b/src/components/DashboardEditor/editorUtils.test.jsx index ef702131dc..5d611e8465 100644 --- a/src/components/DashboardEditor/editorUtils.test.jsx +++ b/src/components/DashboardEditor/editorUtils.test.jsx @@ -5,9 +5,40 @@ import { getDefaultCard, isCardJsonValid, renderBreakpointInfo, + formatSeries, + formatAttributes, + handleDataSeriesChange, + handleDataItemEdit, } from './editorUtils'; describe('editorUtils', () => { + const cardConfig = { + id: 'Timeseries', + title: 'Untitled', + size: 'MEDIUMWIDE', + type: 'TIMESERIES', + content: { + series: [ + { + label: 'Temperature', + dataSourceId: 'temperature', + color: 'red', + }, + { + label: 'Pressure', + dataSourceId: 'pressure', + }, + ], + xLabel: 'Time', + yLabel: 'Temperature (˚F)', + includeZeroOnXaxis: true, + includeZeroOnYaxis: true, + timeDataSourceId: 'timestamp', + addSpaceOnEdges: 1, + }, + interval: 'day', + }; + const mockValueCard = { id: 'Standard', title: 'value card', @@ -33,7 +64,18 @@ describe('editorUtils', () => { title: 'timeseries card', type: 'TIMESERIES', size: 'MEDIUM', - content: {}, + content: { + series: [ + { + dataSourceId: 'airflow', + label: 'Airflow', + }, + { + dataSourceId: 'torque', + label: 'Torque', + }, + ], + }, }; const mockBarChartCard = { id: 'Standard', @@ -150,4 +192,192 @@ describe('editorUtils', () => { expect(renderBreakpointInfo('md', i18n)).toEqual('Md'); }); }); + describe('formatSeries', () => { + const cardConfigWithoutColorDefinition = { + content: { + series: [ + { + label: 'Temperature', + dataSourceId: 'temperature', + }, + { + label: 'Pressure', + dataSourceId: 'pressure', + }, + ], + }, + }; + const selectedItems = [ + { id: 'temperature', text: 'Temperature' }, + { id: 'pressure', text: 'Pressure' }, + ]; + it('should correctly format the card series', () => { + expect(formatSeries(selectedItems, cardConfig)).toEqual([ + { dataSourceId: 'temperature', label: 'Temperature', color: 'red' }, + { dataSourceId: 'pressure', label: 'Pressure', color: '#1192e8' }, + ]); + }); + it('should correctly generate colors for dataItems with no color defined', () => { + expect( + formatSeries(selectedItems, cardConfigWithoutColorDefinition) + ).toEqual([ + { dataSourceId: 'temperature', label: 'Temperature', color: '#6929c4' }, + { dataSourceId: 'pressure', label: 'Pressure', color: '#1192e8' }, + ]); + }); + }); + describe('formatAttributes', () => { + it('should correctly format the card attributes', () => { + const mockValueCard2 = { + id: 'Standard', + title: 'value card', + type: 'VALUE', + size: 'MEDIUM', + content: { + attributes: [ + { + dataSourceId: 'key1', + unit: '%', + precision: 2, + thresholds: [], + dataFilter: { deviceid: '73000' }, + }, + { + dataSourceId: 'key2', + unit: 'lb', + label: 'Key 2', + }, + ], + }, + }; + const selectedItems = [ + { id: 'key1', text: 'Key 1' }, + { id: 'key2', text: 'Key 2' }, + ]; + expect(formatAttributes(selectedItems, mockValueCard2)).toEqual([ + { + dataSourceId: 'key1', + label: 'key1', + precision: 2, + dataFilter: { deviceid: '73000' }, + }, + { dataSourceId: 'key2', label: 'Key 2' }, + ]); + }); + }); + describe('handleDataSeriesChange', () => { + it('should correctly format the data in Timeseries', () => { + const selectedItems = [ + { id: 'key1', text: 'Key 1' }, + { id: 'key2', text: 'Key 2' }, + ]; + const newCard = handleDataSeriesChange(selectedItems, mockTimeSeriesCard); + expect(newCard).toEqual({ + content: { + series: [ + { + color: '#6929c4', + dataSourceId: 'key1', + label: 'key1', + }, + { + color: '#1192e8', + dataSourceId: 'key2', + label: 'key2', + }, + ], + }, + id: 'Standard', + size: 'MEDIUM', + title: 'timeseries card', + type: 'TIMESERIES', + }); + }); + it('should correctly format the data in Value', () => { + const selectedItems = [ + { id: 'key1', text: 'Key 1' }, + { id: 'key2', text: 'Key 2' }, + ]; + const newCard = handleDataSeriesChange(selectedItems, mockValueCard); + expect(newCard).toEqual({ + content: { + attributes: [ + { + dataSourceId: 'key1', + label: 'Key 1', + }, + { + dataSourceId: 'key2', + label: 'Key 2', + }, + ], + }, + id: 'Standard', + size: 'MEDIUM', + title: 'value card', + type: 'VALUE', + }); + }); + }); + describe('handleDataItemEdit', () => { + it('should correctly format the data in Timeseries', () => { + const editDataItem = { + dataSourceId: 'torque', + label: 'Torque', + xLabel: 'X axis', + yLabel: 'Y axis', + unit: 'PSI', + }; + const newCard = handleDataItemEdit(editDataItem, mockTimeSeriesCard); + expect(newCard).toEqual({ + id: 'Standard', + title: 'timeseries card', + type: 'TIMESERIES', + size: 'MEDIUM', + content: { + series: [ + { + dataSourceId: 'airflow', + label: 'Airflow', + }, + { + dataSourceId: 'torque', + label: 'Torque', + xLabel: 'X axis', + yLabel: 'Y axis', + unit: 'PSI', + }, + ], + }, + }); + }); + it('should correctly format the data in Value', () => { + const editDataItem = { + dataSourceId: 'key2', + unit: 'F', + label: 'Updated Key 2', + }; + const newCard = handleDataItemEdit(editDataItem, mockValueCard); + expect(newCard).toEqual({ + id: 'Standard', + title: 'value card', + type: 'VALUE', + size: 'MEDIUM', + content: { + attributes: [ + { + dataSourceId: 'key1', + unit: '%', + label: 'Key 1', + }, + { + dataSourceId: 'key2', + unit: 'F', + label: 'Updated Key 2', + }, + ], + }, + }); + }); + }); }); diff --git a/src/components/Dropdown/__snapshots__/Dropdown.story.storyshot b/src/components/Dropdown/__snapshots__/Dropdown.story.storyshot index 60660ee4ee..e40efd575f 100644 --- a/src/components/Dropdown/__snapshots__/Dropdown.story.storyshot +++ b/src/components/Dropdown/__snapshots__/Dropdown.story.storyshot @@ -26,8 +26,8 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd > @@ -41,10 +41,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-14-label downshift-14-toggle-button" + aria-labelledby="downshift-30-label downshift-30-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-14-toggle-button" + id="downshift-30-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -76,9 +76,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd
@@ -161,10 +161,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-15-label downshift-15-toggle-button" + aria-labelledby="downshift-31-label downshift-31-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-15-toggle-button" + id="downshift-31-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -196,9 +196,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd
@@ -277,10 +277,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-16-label downshift-16-toggle-button" + aria-labelledby="downshift-32-label downshift-32-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-16-toggle-button" + id="downshift-32-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -312,9 +312,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd
@@ -456,10 +456,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-17-label downshift-17-toggle-button" + aria-labelledby="downshift-33-label downshift-33-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-17-toggle-button" + id="downshift-33-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -491,9 +491,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Dropd
@@ -52,10 +52,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-18-label downshift-18-toggle-button" + aria-labelledby="downshift-34-label downshift-34-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-18-toggle-button" + id="downshift-34-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -90,9 +90,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Exper
{ + const [selectedIcon, setSelectedIcon] = useState(selectedIconProp); + + const renderIconItem = (item) => ( +
+
+ {item.carbonIcon} +
+
+ ); + + return ( + { + setSelectedIcon(selectedItem); + onChange({ icon: selectedItem }); + }} + selectedItem={selectedIcon || icons[0]} + titleText={titleText} + type="default" + test-id={testID} + /> + ); +}; + +SimpleIconDropdown.propTypes = propTypes; +SimpleIconDropdown.defaultProps = defaultProps; + +export default SimpleIconDropdown; diff --git a/src/components/SimpleIconDropdown/SimpleIconDropdown.story.jsx b/src/components/SimpleIconDropdown/SimpleIconDropdown.story.jsx new file mode 100644 index 0000000000..73e6a50b89 --- /dev/null +++ b/src/components/SimpleIconDropdown/SimpleIconDropdown.story.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { withKnobs, text } from '@storybook/addon-knobs'; + +import { validThresholdIcons } from '../DashboardEditor/editorUtils'; + +import SimpleIconDropdown from './SimpleIconDropdown'; + +export default { + title: 'Watson IoT Experimental/SimpleIconDropdown', + decorators: [withKnobs], + parameters: { + component: SimpleIconDropdown, + }, + excludeStories: [], +}; + +export const DefaultExample = () => ( +
+ +
+); + +export const WithDefinedIconColors = () => ( +
+ ({ ...icon, color: 'red' }))} + onChange={action('onChange')} + /> +
+); + +export const WithSelectedIcon = () => ( +
+ +
+); + +DefaultExample.story = { + parameters: { + info: { + propTables: [SimpleIconDropdown], + propTablesExclude: [], + }, + }, +}; diff --git a/src/components/SimpleIconDropdown/SimpleIconDropdown.test.jsx b/src/components/SimpleIconDropdown/SimpleIconDropdown.test.jsx new file mode 100644 index 0000000000..91c1ada3ce --- /dev/null +++ b/src/components/SimpleIconDropdown/SimpleIconDropdown.test.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import SimpleIconDropdown from './SimpleIconDropdown'; + +describe('SimpleIconDropdown', () => { + const mockOnChange = jest.fn(); + + const commonProps = { + id: 'myIconDropdown', + titleText: 'icon', + onChange: mockOnChange, + }; + + it('clicking card should select the card and close gallery', async () => { + render(); + + const iconDropdown = screen.getByRole('button'); + expect(iconDropdown).toBeInTheDocument(); + }); +}); diff --git a/src/components/SimpleIconDropdown/__snapshots__/SimpleIconDropdown.story.storyshot b/src/components/SimpleIconDropdown/__snapshots__/SimpleIconDropdown.story.storyshot new file mode 100644 index 0000000000..1b6011af6b --- /dev/null +++ b/src/components/SimpleIconDropdown/__snapshots__/SimpleIconDropdown.story.storyshot @@ -0,0 +1,434 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/SimpleIconDropdown Default Example 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+
+ +
+`; + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/SimpleIconDropdown With Defined Icon Colors 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+
+ +
+`; + +exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT Experimental/SimpleIconDropdown With Selected Icon 1`] = ` +
+
+
+
+ +
+ +
+
+
+
+
+ +
+`; diff --git a/src/components/SimpleIconDropdown/_simple-icon-dropdown.scss b/src/components/SimpleIconDropdown/_simple-icon-dropdown.scss new file mode 100644 index 0000000000..a4c9cd629d --- /dev/null +++ b/src/components/SimpleIconDropdown/_simple-icon-dropdown.scss @@ -0,0 +1,57 @@ +@import '../../globals/vars'; + +.#{$iot-prefix}--icon-dropdown { + .#{$prefix}--list-box__menu-item__option { + margin: 0; + padding: 0; + border-top: none; + } + + .#{$iot-prefix}--icon-dropdown__item { + display: flex; + flex-direction: row; + height: 100%; + padding-left: 1rem; + padding-right: 1rem; + } + + .#{$iot-prefix}--icon-dropdown__item-border { + display: flex; + align-items: center; + } + + // When showing selected item we must remove the padding and border. + .#{$prefix}--list-box__label .#{$iot-prefix}--icon-dropdown__item { + padding-left: 0; + .#{$iot-prefix}--icon-dropdown__item-border { + border-color: transparent; + } + } +} + +.#{$iot-prefix}--color-dropdown__icon-sample { + width: 1.5rem; + height: 1.5rem; + margin-right: $spacing-04; + flex-shrink: 0; +} + +.#{$iot-prefix}--icon-dropdown__icon-name { + overflow: hidden; + text-overflow: ellipsis; +} + +html[dir='rtl'] { + .#{$iot-prefix}--icon-dropdown__icon-sample { + margin-left: $spacing-04; + } + + .#{$iot-prefix}--icon-dropdown__item { + padding-right: 2rem; + } + + // When showing selected item we must remove the padding. + .#{$prefix}--list-box__label .#{$iot-prefix}--icon-dropdown__item { + padding-right: 0; + } +} diff --git a/src/components/Table/TableViewDropdown/__snapshots__/TableViewDropdown.story.storyshot b/src/components/Table/TableViewDropdown/__snapshots__/TableViewDropdown.story.storyshot index d8ad252a22..0b9ac07448 100644 --- a/src/components/Table/TableViewDropdown/__snapshots__/TableViewDropdown.story.storyshot +++ b/src/components/Table/TableViewDropdown/__snapshots__/TableViewDropdown.story.storyshot @@ -38,10 +38,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Table aria-disabled={false} aria-expanded={false} aria-haspopup="listbox" - aria-labelledby="downshift-32-label downshift-32-toggle-button" + aria-labelledby="downshift-51-label downshift-51-toggle-button" className="bx--list-box__field" disabled={false} - id="downshift-32-toggle-button" + id="downshift-51-toggle-button" onClick={[Function]} onKeyDown={[Function]} type="button" @@ -108,9 +108,9 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Table
pick an option @@ -124330,10 +124330,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Table
pick an option @@ -128745,10 +128745,10 @@ exports[`Storybook Snapshot tests and console checks Storyshots Watson IoT/Table
{ // Checks size property against new size naming convention and reassigns to closest supported size if necessary. const newSize = getUpdatedCardSize(size); - // matching threshold will be the first match in the list, or a value of null - const matchingThreshold = thresholds - .filter((t) => { - switch (t.comparison) { - case '<': - return !isNil(value) && value < t.value; - case '>': - return value > t.value; - case '=': - return value === t.value; - case '<=': - return !isNil(value) && value <= t.value; - case '>=': - return value >= t.value; - default: - return false; - } - }) - .concat([null])[0]; + // matching threshold will be the first match in the list, or a value of null if not isEditable + const matchingThreshold = isEditable + ? thresholds[0] + : thresholds + .filter((t) => { + switch (t.comparison) { + case '<': + return !isNil(value) && value < t.value; + case '>': + return value > t.value; + case '=': + return value === t.value; + case '<=': + return !isNil(value) && value <= t.value; + case '>=': + return value >= t.value; + default: + return false; + } + }) + .concat([null])[0]; const valueColor = matchingThreshold && matchingThreshold.icon === undefined ? matchingThreshold.color diff --git a/src/components/ValueCard/ValueCard.jsx b/src/components/ValueCard/ValueCard.jsx index 2cb4aff735..fca21e2d57 100644 --- a/src/components/ValueCard/ValueCard.jsx +++ b/src/components/ValueCard/ValueCard.jsx @@ -370,6 +370,7 @@ const ValueCard = ({ ? 'center' : undefined } + isEditable={isEditable} {...attribute} renderIconByName={others.renderIconByName} size={newSize} // When the card is in the editable state, we will show a preview diff --git a/src/styles.scss b/src/styles.scss index 544b54ff2c..54d820ea87 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -156,6 +156,7 @@ $deprecations--message: 'Deprecated code was found, this code will be removed be //------------------------------------- @import 'components/UIShell/ui-shell'; @import 'components/ColorDropdown/color-dropdown'; +@import 'components/SimpleIconDropdown/simple-icon-dropdown'; //------------------------------------- // 🙈 Hidden (Not exposed on website) diff --git a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap index d66d0b3eed..84ad2d9a4e 100644 --- a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap +++ b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap @@ -8108,6 +8108,7 @@ Map { }, "DashboardEditor" => Object { "defaultProps": Object { + "availableDimensions": Object {}, "breakpointSwitcher": null, "dataItems": Array [], "getValidDataItems": null, @@ -8165,6 +8166,12 @@ Map { "title": "", }, "propTypes": Object { + "availableDimensions": Object { + "args": Array [ + Object {}, + ], + "type": "shape", + }, "breakpointSwitcher": Object { "args": Array [ Object { @@ -8347,6 +8354,7 @@ Map { }, "CardEditor" => Object { "defaultProps": Object { + "availableDimensions": Object {}, "cardConfig": null, "dataItems": Array [], "getValidDataItems": null, @@ -8371,6 +8379,12 @@ Map { ], }, "propTypes": Object { + "availableDimensions": Object { + "args": Array [ + Object {}, + ], + "type": "shape", + }, "cardConfig": Object { "args": Array [ Object { @@ -17816,6 +17830,7 @@ Map { "name": "cyan90", }, ], + "hideLabels": false, "label": "Select a color", "light": false, "selectedColor": undefined, @@ -17841,6 +17856,9 @@ Map { ], "type": "arrayOf", }, + "hideLabels": Object { + "type": "bool", + }, "id": Object { "isRequired": true, "type": "string",