feat(plugin-chart-table): Implement showing totals (#1034)

* feat(plugin-chart-table): implement totals row

* Fix typo

* Fix totals with percentage metrics

* Code review fixes

* Use dnd with percentage metrics and sortby controls

* Make totals checkbox tooltip more descriptive

* Remove console.log

* Change totals tooltip

* Fix typing error

* Use array destructuring

* Fix typo
This commit is contained in:
Kamil Gabryjelski 2021-04-06 16:31:40 +02:00 committed by Yongjie Zhao
parent 62906906a5
commit f4eeebf368
13 changed files with 228 additions and 91 deletions

View File

@ -111,3 +111,15 @@ export const dnd_adhoc_metric: SharedControlConfig<'DndMetricSelect'> = {
description: t('Metric'),
default: (c: Control) => mainMetric(c.savedMetrics),
};
export const dnd_timeseries_limit_metric: SharedControlConfig<'DndMetricSelect'> = {
type: 'DndMetricSelect',
label: t('Sort by'),
default: null,
description: t('Metric used to define the top series'),
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: datasource?.metrics || [],
datasourceType: datasource?.type,
}),
};

View File

@ -69,6 +69,7 @@ import {
dnd_adhoc_filters,
dnd_adhoc_metric,
dnd_adhoc_metrics,
dnd_timeseries_limit_metric,
dndColumnsControl,
dndEntity,
dndGroupByControl,
@ -480,7 +481,7 @@ const sharedControls = {
time_range,
row_limit,
limit,
timeseries_limit_metric,
timeseries_limit_metric: enableExploreDnd ? dnd_timeseries_limit_metric : timeseries_limit_metric,
series: enableExploreDnd ? dndSeries : series,
entity: enableExploreDnd ? dndEntity : entity,
x,

View File

@ -29,14 +29,14 @@
"@emotion/core": "^10.0.28",
"@superset-ui/chart-controls": "0.17.27",
"@superset-ui/core": "0.17.27",
"@types/d3-array": "^2.0.0",
"@types/react-table": "^7.0.19",
"@types/d3-array": "^2.9.0",
"@types/react-table": "^7.0.29",
"d3-array": "^2.4.0",
"match-sorter": "^6.1.0",
"match-sorter": "^6.3.0",
"memoize-one": "^5.1.1",
"react-table": "^7.2.1",
"regenerator-runtime": "^0.13.5",
"xss": "^1.0.6"
"react-table": "^7.6.3",
"regenerator-runtime": "^0.13.7",
"xss": "^1.0.8"
},
"peerDependencies": {
"@types/react": "*",

View File

@ -35,6 +35,7 @@ import {
IdType,
Row,
} from 'react-table';
import { t } from '@superset-ui/core';
import { matchSorter, rankings } from 'match-sorter';
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
import SelectPageSize, { SelectPageSizeProps, SizeOption } from './components/SelectPageSize';
@ -44,6 +45,8 @@ import { PAGE_SIZE_OPTIONS } from '../consts';
export interface DataTableProps<D extends object> extends TableOptions<D> {
tableClassName?: string;
totals?: { value: string; className?: string }[];
totalsHeaderSpan?: number;
searchInput?: boolean | GlobalFilterProps<D>['searchInput'];
selectPageSize?: boolean | SelectPageSizeProps['selectRenderer'];
pageSizeOptions?: SizeOption[]; // available page size options
@ -70,6 +73,8 @@ export default function DataTable<D extends object>({
tableClassName,
columns,
data,
totals,
totalsHeaderSpan,
serverPaginationData,
width: initialWidth = '100%',
height: initialHeight = 300,
@ -229,6 +234,16 @@ export default function DataTable<D extends object>({
</tr>
)}
</tbody>
{totals && (
<tfoot>
<tr key="totals" className="dt-totals">
<td colSpan={totalsHeaderSpan}>{t('Totals')}</td>
{totals.map(item => (
<td className={item.className}>{item.value}</td>
))}
</tr>
</tfoot>
)}
</table>
);

View File

@ -43,10 +43,11 @@ type TrWithTh = ReactElementWithChildren<'tr', Th[]>;
type TrWithTd = ReactElementWithChildren<'tr', Td[]>;
type Thead = ReactElementWithChildren<'thead', TrWithTh>;
type Tbody = ReactElementWithChildren<'tbody', TrWithTd>;
type Tfoot = ReactElementWithChildren<'tfoot', TrWithTd>;
type Col = ReactElementWithChildren<'col', null>;
type ColGroup = ReactElementWithChildren<'colgroup', Col>;
export type Table = ReactElementWithChildren<'table', (Thead | Tbody | ColGroup)[]>;
export type Table = ReactElementWithChildren<'table', (Thead | Tbody | Tfoot | ColGroup)[]>;
export type TableRenderer = () => Table;
export type GetTableSize = () => Partial<StickyState> | undefined;
export type SetStickyState = (size?: Partial<StickyState>) => void;
@ -118,11 +119,18 @@ function StickyWrap({
}
let thead: Thead | undefined;
let tbody: Tbody | undefined;
let tfoot: Tfoot | undefined;
React.Children.forEach(table.props.children, node => {
if (!node) {
return;
}
if (node.type === 'thead') {
thead = node;
} else if (node.type === 'tbody') {
tbody = node;
} else if (node.type === 'tfoot') {
tfoot = node;
}
});
if (!thead || !tbody) {
@ -134,7 +142,9 @@ function StickyWrap({
}, [thead]);
const theadRef = useRef<HTMLTableSectionElement>(null); // original thead for layout computation
const tfootRef = useRef<HTMLTableSectionElement>(null); // original tfoot for layout computation
const scrollHeaderRef = useRef<HTMLDivElement>(null); // fixed header
const scrollFooterRef = useRef<HTMLDivElement>(null); // fixed footer
const scrollBodyRef = useRef<HTMLDivElement>(null); // main body
const scrollBarSize = getScrollBarSize();
@ -147,47 +157,51 @@ function StickyWrap({
// update scrollable area and header column sizes when mounted
useLayoutEffect(() => {
if (theadRef.current) {
const bodyThead = theadRef.current;
const theadHeight = bodyThead.clientHeight;
if (!theadHeight) {
return;
}
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement).clientHeight;
const ths = bodyThead.childNodes[0].childNodes as NodeListOf<HTMLTableHeaderCellElement>;
const widths = Array.from(ths).map(th => th.clientWidth);
const [hasVerticalScroll, hasHorizontalScroll] = needScrollBar({
width: maxWidth,
height: maxHeight - theadHeight,
innerHeight: fullTableHeight,
innerWidth: widths.reduce(sum),
scrollBarSize,
});
// real container height, include table header and space for
// horizontal scroll bar
const realHeight = Math.min(
maxHeight,
hasHorizontalScroll ? fullTableHeight + scrollBarSize : fullTableHeight,
);
setStickyState({
hasVerticalScroll,
hasHorizontalScroll,
setStickyState,
width: maxWidth,
height: maxHeight,
realHeight,
tableHeight: fullTableHeight,
bodyHeight: realHeight - theadHeight,
columnWidths: widths,
});
if (!theadRef.current) {
return;
}
const bodyThead = theadRef.current;
const theadHeight = bodyThead.clientHeight;
const tfootHeight = tfootRef.current ? tfootRef.current.clientHeight : 0;
if (!theadHeight) {
return;
}
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement).clientHeight;
const ths = bodyThead.childNodes[0].childNodes as NodeListOf<HTMLTableHeaderCellElement>;
const widths = Array.from(ths).map(th => th.clientWidth);
const [hasVerticalScroll, hasHorizontalScroll] = needScrollBar({
width: maxWidth,
height: maxHeight - theadHeight - tfootHeight,
innerHeight: fullTableHeight,
innerWidth: widths.reduce(sum),
scrollBarSize,
});
// real container height, include table header, footer and space for
// horizontal scroll bar
const realHeight = Math.min(
maxHeight,
hasHorizontalScroll ? fullTableHeight + scrollBarSize : fullTableHeight,
);
setStickyState({
hasVerticalScroll,
hasHorizontalScroll,
setStickyState,
width: maxWidth,
height: maxHeight,
realHeight,
tableHeight: fullTableHeight,
bodyHeight: realHeight - theadHeight - tfootHeight,
columnWidths: widths,
});
}, [maxWidth, maxHeight, setStickyState, scrollBarSize]);
let sizerTable: ReactElement | undefined;
let headerTable: ReactElement | undefined;
let footerTable: ReactElement | undefined;
let bodyTable: ReactElement | undefined;
if (needSizer) {
const theadWithRef = React.cloneElement(thead, { ref: theadRef });
const tfootWithRef = tfoot && React.cloneElement(tfoot, { ref: tfootRef });
sizerTable = (
<div
key="sizer"
@ -197,7 +211,7 @@ function StickyWrap({
visibility: 'hidden',
}}
>
{React.cloneElement(table, {}, theadWithRef, tbody)}
{React.cloneElement(table, {}, theadWithRef, tbody, tfootWithRef)}
</div>
);
}
@ -242,10 +256,26 @@ function StickyWrap({
</div>
);
footerTable = tfoot && (
<div
key="footer"
ref={scrollFooterRef}
style={{
overflow: 'hidden',
}}
>
{React.cloneElement(table, mergeStyleProp(table, fixedTableLayout), headerColgroup, tfoot)}
{footerTable}
</div>
);
const onScroll: UIEventHandler<HTMLDivElement> = e => {
if (scrollHeaderRef.current) {
scrollHeaderRef.current.scrollLeft = e.currentTarget.scrollLeft;
}
if (scrollFooterRef.current) {
scrollFooterRef.current.scrollLeft = e.currentTarget.scrollLeft;
}
};
bodyTable = (
<div
@ -272,6 +302,7 @@ function StickyWrap({
>
{headerTable}
{bodyTable}
{footerTable}
{sizerTable}
</div>
);

View File

@ -38,6 +38,9 @@ export default styled.div`
.dt-metric {
text-align: right;
}
.dt-totals {
font-weight: bold;
}
.dt-is-null {
color: ${({ theme: { colors } }) => colors.grayscale.light1};
}

View File

@ -16,20 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useMemo, useCallback, CSSProperties } from 'react';
import { ColumnInstance, DefaultSortTypes, ColumnWithLooseAccessor } from 'react-table';
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
import { ColumnInstance, ColumnWithLooseAccessor, DefaultSortTypes } from 'react-table';
import { extent as d3Extent, max as d3Max } from 'd3-array';
import { FaSort, FaSortUp as FaSortAsc, FaSortDown as FaSortDesc } from 'react-icons/fa';
import {
t,
tn,
DataRecordValue,
DataRecord,
GenericDataType,
getNumberFormatter,
} from '@superset-ui/core';
import { FaSort, FaSortDown as FaSortDesc, FaSortUp as FaSortAsc } from 'react-icons/fa';
import { DataRecord, DataRecordValue, GenericDataType, t, tn } from '@superset-ui/core';
import { TableChartTransformedProps, DataColumnMeta } from './types';
import { DataColumnMeta, TableChartTransformedProps } from './types';
import DataTable, {
DataTableProps,
SearchInputProps,
@ -38,7 +31,7 @@ import DataTable, {
} from './DataTable';
import Styles from './Styles';
import formatValue from './utils/formatValue';
import { formatColumnValue } from './utils/formatValue';
import { PAGE_SIZE_OPTIONS } from './consts';
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
@ -154,6 +147,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
height,
width,
data,
totals,
isRawRecords,
rowCount = 0,
columns: columnsMeta,
@ -220,7 +214,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const getColumnConfigs = useCallback(
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
const { key, label, dataType, isMetric, formatter, config = {} } = column;
const { key, label, dataType, isMetric, config = {} } = column;
const isNumber = dataType === GenericDataType.NUMERIC;
const isFilter = !isNumber && emitFilter;
const textAlign = config.horizontalAlign
@ -241,10 +235,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
config.alignPositiveNegative === undefined ? defaultAlignPN : config.alignPositiveNegative;
const colorPositiveNegative =
config.colorPositiveNegative === undefined ? defaultColorPN : config.colorPositiveNegative;
const smallNumberFormatter =
config.d3SmallNumberFormat === undefined
? formatter
: getNumberFormatter(config.d3SmallNumberFormat);
const valueRange =
(config.showCellBars === undefined ? showCellBars : config.showCellBars) &&
@ -263,12 +253,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// so we ask TS not to check.
accessor: ((datum: D) => datum[key]) as never,
Cell: ({ value }: { column: ColumnInstance<D>; value: DataRecordValue }) => {
const [isHtml, text] = formatValue(
isNumber && typeof value === 'number' && Math.abs(value) < 1
? smallNumberFormatter
: formatter,
value,
);
const [isHtml, text] = formatColumnValue(column, value);
const html = isHtml ? { __html: text } : undefined;
const cellProps = {
// show raw number in title in case of numeric values
@ -346,10 +331,31 @@ export default function TableChart<D extends DataRecord = DataRecord>(
updateExternalFormData(setDataMask, pageNumber, pageSize);
};
const totalsFormatted =
totals &&
columnsMeta
.filter(column => Object.keys(totals).includes(column.key))
.reduce(
(acc: { value: string; className: string }[], column) => [
...acc,
{
value: formatColumnValue(column, totals[column.key])[1],
className: column.dataType === GenericDataType.NUMERIC ? 'dt-metric' : '',
},
],
[],
);
const totalsHeaderSpan =
totalsFormatted &&
columnsMeta.filter(column => !column.isPercentMetric).length - totalsFormatted.length;
return (
<Styles>
<DataTable<D>
columns={columns}
totals={totalsFormatted}
totalsHeaderSpan={totalsHeaderSpan}
data={data}
rowCount={rowCount}
tableClassName="table table-striped table-condensed"

View File

@ -18,11 +18,11 @@
*/
import {
buildQueryContext,
ensureIsArray,
getMetricLabel,
QueryMode,
removeDuplicates,
ensureIsArray,
QueryObject,
removeDuplicates,
} from '@superset-ui/core';
import { PostProcessingRule } from '@superset-ui/core/src/query/types/PostProcessing';
import { BuildQuery } from '@superset-ui/core/src/chart/registries/ChartBuildQueryRegistrySingleton';
@ -115,13 +115,25 @@ const buildQuery: BuildQuery<TableChartFormData> = (formData: TableChartFormData
// Because we use same buildQuery for all table on the page we need split them by id
options?.hooks?.setCachedChanges({ [formData.slice_id]: queryObject.filters });
const extraQueries: QueryObject[] = [];
if (metrics && formData.show_totals && queryMode === QueryMode.aggregate) {
extraQueries.push({
...queryObject,
columns: [],
row_limit: 0,
row_offset: 0,
post_processing: [],
});
}
if (formData.server_pagination) {
return [
{ ...queryObject },
{ ...queryObject, row_limit: 0, row_offset: 0, post_processing: [], is_rowcount: true },
...extraQueries,
];
}
return [queryObject];
return [queryObject, ...extraQueries];
});
};

View File

@ -25,6 +25,8 @@ import {
QueryMode,
QueryFormColumn,
ChartDataResponseResult,
isFeatureEnabled,
FeatureFlag,
} from '@superset-ui/core';
import {
D3_TIME_FORMAT_OPTIONS,
@ -111,6 +113,11 @@ const percent_metrics: typeof sharedControls.metrics = {
validators: [],
};
const dnd_percent_metrics = {
...percent_metrics,
type: 'DndMetricSelect',
};
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
@ -148,7 +155,9 @@ const config: ControlPanelConfig = {
[
{
name: 'percent_metrics',
config: percent_metrics,
config: isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dnd_percent_metrics
: percent_metrics,
},
],
[
@ -230,6 +239,20 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'show_totals',
config: {
type: 'CheckboxControl',
label: t('Show totals'),
default: true,
description: t(
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
),
visibility: isAggMode,
},
},
],
['adhoc_filters'],
],
},
@ -334,7 +357,7 @@ const config: ControlPanelConfig = {
name: 'column_config',
config: {
type: 'ColumnConfigControl',
label: t('Cuztomize columns'),
label: t('Customize columns'),
description: t('Further customize how to display each column'),
renderTrigger: true,
mapStateToProps(explore, control, chart) {

View File

@ -183,7 +183,7 @@ const transformProps = (chartProps: TableChartProps): TableChartTransformedProps
height,
width,
rawFormData: formData,
queriesData,
queriesData = [],
initialValues: filters = {},
ownCurrentState: serverPaginationData = {},
hooks: { onAddFilter: onChangeFilter, setDataMask = () => {} },
@ -200,17 +200,31 @@ const transformProps = (chartProps: TableChartProps): TableChartTransformedProps
server_page_length: serverPageLength = 10,
order_desc: sortDesc = false,
query_mode: queryMode,
show_totals: showTotals,
} = formData;
const [metrics, percentMetrics, columns] = processColumns(chartProps);
const data = processDataRecords(queriesData?.[0]?.data, columns);
const rowCount = queriesData?.[1]?.data?.[0]?.rowcount as number;
let baseQuery;
let countQuery;
let totalQuery;
let rowCount;
if (serverPagination) {
[baseQuery, countQuery, totalQuery] = queriesData;
rowCount = (countQuery?.data?.[0]?.rowcount as number) ?? 0;
} else {
[baseQuery, totalQuery] = queriesData;
rowCount = baseQuery?.rowcount ?? 0;
}
const data = processDataRecords(baseQuery?.data, columns);
const totals = showTotals && queryMode === QueryMode.aggregate ? totalQuery?.data[0] : undefined;
return {
height,
width,
isRawRecords: queryMode === QueryMode.raw,
data,
totals,
columns,
serverPagination,
metrics,

View File

@ -90,6 +90,7 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
setDataMask: SetDataMaskHook;
isRawRecords?: boolean;
data: D[];
totals?: D;
columns: DataColumnMeta[];
metrics?: (keyof D)[];
percentMetrics?: (keyof D)[];

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { FilterXSS, getDefaultWhiteList } from 'xss';
import { DataRecordValue } from '@superset-ui/core';
import { DataRecordValue, GenericDataType, getNumberFormatter } from '@superset-ui/core';
import { DataColumnMeta } from '../types';
const xss = new FilterXSS({
@ -35,14 +35,15 @@ const xss = new FilterXSS({
function isProbablyHTML(text: string) {
return /<[^>]+>/.test(text);
}
/**
* Format text for cell value.
*/
export default function formatValue(
function formatValue(
formatter: DataColumnMeta['formatter'],
value: DataRecordValue,
): [boolean, string] {
if (value === null) {
if (value === null || typeof value === 'undefined') {
return [false, 'N/A'];
}
if (formatter) {
@ -54,3 +55,16 @@ export default function formatValue(
}
return [false, value.toString()];
}
export function formatColumnValue(column: DataColumnMeta, value: DataRecordValue) {
const { dataType, formatter, config = {} } = column;
const isNumber = dataType === GenericDataType.NUMERIC;
const smallNumberFormatter =
config.d3SmallNumberFormat === undefined
? formatter
: getNumberFormatter(config.d3SmallNumberFormat);
return formatValue(
isNumber && typeof value === 'number' && Math.abs(value) < 1 ? smallNumberFormatter : formatter,
value,
);
}

View File

@ -4689,6 +4689,11 @@
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96"
integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA==
"@types/d3-array@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
integrity sha512-sdBMGfNvLUkBypPMEhOcKcblTQfgHbqbYrUqRE31jOwdDHBJBxz4co2MDAq93S4Cp++phk4UiwoEg/1hK3xXAQ==
"@types/d3-cloud@^1.2.1":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/d3-cloud/-/d3-cloud-1.2.3.tgz#cfaac9cb601968c27094903a82687336de69b518"
@ -5041,10 +5046,10 @@
dependencies:
"@types/react" "*"
"@types/react-table@^7.0.19":
version "7.0.19"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.19.tgz#a70603ac0ffdeaf399fc6919aacb32fc42e9dd40"
integrity sha512-RYyEY7Yry6F2JsKhHeFsGdzuFF1hMqBStQrrazDzpBl4m/ECGHJxFVQjLBRzRwK+47ZKNPm79f7qEpHirbiCLA==
"@types/react-table@^7.0.29":
version "7.0.29"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.29.tgz#af2c82f2d6a39be5bc0f191b30501309a8db0949"
integrity sha512-RCGVKGlTDv3jbj37WJ5HhN3sPb0W/2rqlvyGUtvawnnyrxgI2BGgASvU93rq2jwanVp5J9l1NYAeiGlNhdaBGw==
dependencies:
"@types/react" "*"
@ -15968,10 +15973,10 @@ marksy@^8.0.0:
he "^1.2.0"
marked "^0.3.12"
match-sorter@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.1.0.tgz#7fec6808d94311a35fef7fd842a11634f2361bd7"
integrity sha512-sKPMf4kbF7Dm5Crx0bbfLpokK68PUJ/0STUIOPa1ZmTZEA3lCaPK3gapQR573oLmvdkTfGojzySkIwuq6Z6xRQ==
match-sorter@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.0.tgz#454a1b31ed218cddbce6231a0ecb5fdc549fed01"
integrity sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
@ -19292,10 +19297,10 @@ react-syntax-highlighter@^13.5.0:
prismjs "^1.21.0"
refractor "^3.1.0"
react-table@^7.2.1:
version "7.6.1"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.1.tgz#33ad2bfc58de619678694c5452597544f4864266"
integrity sha512-XQyvombPoFbNiDHWAXdxbN78kFpsT1/aJuRSupFfBhO3FJtEVIIq2xbV1NvLzrd1YwfCYRm07ln5OHlfz0SXBg==
react-table@^7.6.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.3.tgz#76434392b3f62344bdb704f5b227c2f29c1ffb14"
integrity sha512-hfPF13zDLxPMpLKzIKCE8RZud9T/XrRTsaCIf8zXpWZIZ2juCl7qrGpo3AQw9eAetXV5DP7s2GDm+hht7qq5Dw==
react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1:
version "16.13.1"
@ -19715,7 +19720,7 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5, regenerator-runtime@^0.13.7:
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
@ -23318,7 +23323,7 @@ xmlchars@^2.1.1, xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xss@^1.0.6:
xss@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.8.tgz#32feb87feb74b3dcd3d404b7a68ababf10700535"
integrity sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==