From 1a51a92a96f677903e9a305928a58e5c4af6459d Mon Sep 17 00:00:00 2001 From: Ruchi Sharma Date: Tue, 25 Feb 2025 12:13:57 -0800 Subject: [PATCH] Feature/data importer (#9408) * move header to top and define sectiobna nd add preview table * add preview load Signed-off-by: Ruchi Sharma * fix ui and add preview table Signed-off-by: Ruchi Sharma * add data source, indices and mismtach in table Signed-off-by: Ruchi Sharma * revert changes from yml Signed-off-by: Ruchi Sharma * comment data import flag Signed-off-by: Ruchi Sharma * fix line Signed-off-by: Ruchi Sharma * revert validate check and give rows count as constant Signed-off-by: Ruchi Sharma --------- Signed-off-by: Ruchi Sharma --- .../public/components/data_importer_app.tsx | 321 +++++++++++------- .../components/import_type_selector.tsx | 60 ++-- .../public/components/preview_table.scss | 6 + .../public/components/preview_table.tsx | 127 +++++++ .../server/routes/cat_indices.ts | 8 +- .../server/routes/import_file.ts | 3 +- yarn.lock | 101 +++++- 7 files changed, 467 insertions(+), 159 deletions(-) create mode 100644 src/plugins/data_importer/public/components/preview_table.scss create mode 100644 src/plugins/data_importer/public/components/preview_table.tsx diff --git a/src/plugins/data_importer/public/components/data_importer_app.tsx b/src/plugins/data_importer/public/components/data_importer_app.tsx index a511ae370682..2bf9212e197f 100644 --- a/src/plugins/data_importer/public/components/data_importer_app.tsx +++ b/src/plugins/data_importer/public/components/data_importer_app.tsx @@ -12,12 +12,14 @@ import { EuiPage, EuiPageBody, EuiPageContent, - EuiPageContentHeader, EuiPageHeader, + EuiFlexGroup, + EuiFlexItem, EuiTitle, - EuiPageSideBar, - EuiFieldText, + EuiLoadingSpinner, EuiSpacer, + EuiComboBox, + EuiFormRow, } from '@elastic/eui'; import { extname } from 'path'; import { @@ -40,13 +42,10 @@ import { ImportResponse } from '../types'; import { PublicConfigSchema } from '../../config'; import { ImportTextContentBody } from './import_text_content'; import { ImportFileContentBody } from './import_file_content'; -import { - CSV_FILE_TYPE, - CSV_SUPPORTED_DELIMITERS, - PLUGIN_NAME_AS_TITLE, -} from '../../common/constants'; +import { CSV_FILE_TYPE, CSV_SUPPORTED_DELIMITERS } from '../../common/constants'; import { DelimiterSelect } from './delimiter_select'; import { previewFile } from '../lib/preview'; +import { PreviewComponent } from './preview_table'; interface DataImporterPluginAppProps { basename: string; @@ -59,6 +58,8 @@ interface DataImporterPluginAppProps { dataSourceManagement?: DataSourceManagementPluginSetup; } +const ROWS_COUNT = 10; + export const DataImporterPluginApp = ({ basename, notifications, @@ -69,44 +70,70 @@ export const DataImporterPluginApp = ({ dataSourceEnabled, dataSourceManagement, }: DataImporterPluginAppProps) => { - const DataSourceMenu = dataSourceManagement?.ui.getDataSourceMenu(); + const DataSourceMenuComponent = dataSourceManagement?.ui.getDataSourceMenu< + DataSourceSelectableConfig + >(); const [indexName, setIndexName] = useState(); const [importType, setImportType] = useState(IMPORT_CHOICE_FILE); const [disableImport, setDisableImport] = useState(); const [dataType, setDataType] = useState( config.enabledFileTypes.length > 0 ? config.enabledFileTypes[0] : undefined ); + const [filePreviewData, setFilePreviewData] = useState({}); const [inputText, setText] = useState(); const [inputFile, setInputFile] = useState(); const [dataSourceId, setDataSourceId] = useState(); - const [selectedDataSource, setSelectedDataSource] = useState(); - const [showDelimiterChoice, setShowDelimiterChoice] = useState(shouldShowDelimiter()); + const [isLoadingPreview, setIsLoadingPreview] = useState(false); + const [showDelimiterChoice, setShowDelimiterChoice] = useState(false); const [delimiter, setDelimiter] = useState( dataType === CSV_FILE_TYPE ? CSV_SUPPORTED_DELIMITERS[0] : undefined ); + const [visibleRows, setVisibleRows] = useState(ROWS_COUNT); + const [indexOptions, setIndexOptions] = useState>([]); + const [createMode, setCreateMode] = useState(false); + + useEffect(() => { + const fetchIndices = async () => { + try { + const response = await http.get('/api/data_importer/_cat_indices'); + setIndexOptions(response.indices.map((index: string) => ({ label: index }))); + } catch (error) { + notifications.toasts.addDanger( + i18n.translate('dataImporter.indicesFetchError', { + defaultMessage: `Failed to fetch indices: {error}`, + values: { error }, + }) + ); + } + }; + + fetchIndices(); + }, [http, notifications.toasts]); const onImportTypeChange = (type: ImportChoices) => { if (type === IMPORT_CHOICE_FILE) { setInputFile(undefined); + setShowDelimiterChoice(true); } else if (type === IMPORT_CHOICE_TEXT) { setText(undefined); + setShowDelimiterChoice(false); } setImportType(type); }; - const onIndexNameChange = (e: any) => { - setIndexName(e.target.value); - }; - const onDataTypeChange = (type: string) => { if (type !== CSV_FILE_TYPE) { setDelimiter(undefined); } setDataType(type); + setShowDelimiterChoice(type === CSV_FILE_TYPE && importType === IMPORT_CHOICE_FILE); }; const onFileInput = (file?: File) => { setInputFile(file); + if (!file) { + setFilePreviewData({}); + } }; const onTextInput = (text: string) => { @@ -120,37 +147,56 @@ export const DataImporterPluginApp = ({ const onDataSourceSelect = (newDataSource: DataSourceOption[]) => { if (newDataSource.length > 0) { setDataSourceId(newDataSource[0].id); - setSelectedDataSource(newDataSource[0]); } }; + const onIndexNameChange = (selected: Array<{ label: string }>) => { + if (selected.length) { + setIndexName(selected[0].label); + setCreateMode(false); + } else { + setIndexName(''); + } + }; + + const onCreateIndexName = (createdOption: string) => { + setIndexName(createdOption); + setCreateMode(true); + }; + const previewData = async () => { if (importType === IMPORT_CHOICE_FILE) { if (inputFile) { const fileExtension = extname(inputFile.name); - const response = await previewFile( - http, - inputFile, - // TODO This should be determined from the index name textbox/selectable - false, - // TODO This should be determined from the file type selectable - fileExtension, - indexName!, - delimiter, - dataSourceId - ); - if (response) { - notifications.toasts.addSuccess( - i18n.translate('dataImporter.previewSuccess', { - defaultMessage: `Preview successful`, - }) - ); - } else { - notifications.toasts.addDanger( - i18n.translate('dataImporter.previewFailed', { - defaultMessage: `Preview failed`, - }) + setIsLoadingPreview(true); // Set loading state to true + try { + const response = await previewFile( + http, + inputFile, + createMode, + fileExtension, + indexName!, + delimiter, + dataSourceId ); + setIsLoadingPreview(false); // Set loading state to false + if (response) { + setFilePreviewData(response); + notifications.toasts.addSuccess( + i18n.translate('dataImporter.previewSuccess', { + defaultMessage: `Preview successful`, + }) + ); + } else { + notifications.toasts.addDanger( + i18n.translate('dataImporter.previewFailed', { + defaultMessage: `Preview failed`, + }) + ); + } + } catch (error) { + // console.error(error, 'error'); + setIsLoadingPreview(false); } } } @@ -165,8 +211,7 @@ export const DataImporterPluginApp = ({ http, inputFile!, indexName!, - // TODO This should be determined from the index name textbox/selectable - false, + createMode, // TODO This should be determined from the file type selectable extname(inputFile!.name), delimiter, @@ -241,18 +286,22 @@ export const DataImporterPluginApp = ({ const renderDataSourceComponent = useMemo(() => { return (
- - + {DataSourceMenuComponent && ( + <> + {}, // Add a proper handler if needed + }} + onManageDataSource={() => {}} + /> + + + )}
); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -270,98 +319,116 @@ export const DataImporterPluginApp = ({ } function shouldShowDelimiter() { - let inputFileType; - if (inputFile) { - const fileExtention = extname(inputFile.name).toLowerCase(); - inputFileType = fileExtention.startsWith('.') ? fileExtention.slice(1) : fileExtention; - } - return ( - (importType === IMPORT_CHOICE_FILE && inputFile && inputFileType === CSV_FILE_TYPE) || - (importType === IMPORT_CHOICE_TEXT && dataType === CSV_FILE_TYPE) - ); + return importType === IMPORT_CHOICE_FILE && dataType === CSV_FILE_TYPE; } + const loadMoreRows = () => { + setVisibleRows((prevVisibleRows) => prevVisibleRows + ROWS_COUNT); + }; + return ( <> - - - {showDelimiterChoice && ( - - )} - - - {i18n.translate('dataImporter.dataSource', { - defaultMessage: 'Data Source Options', - })} - - - - - {dataSourceEnabled && renderDataSourceComponent} - - Import - - - - Preview - -

- +

- - -

- {importType === IMPORT_CHOICE_TEXT && ( - - )} - {importType === IMPORT_CHOICE_FILE && ( - + + + {dataSourceEnabled && ( + <> + + + {i18n.translate('dataImporter.dataSource', { + defaultMessage: 'Select target data source', + })} + + + {renderDataSourceComponent} + + )} + + + + {i18n.translate('dataImporter.indexName', { + defaultMessage: 'Create/Select Index Name', + })} + + + + + + + {importType === IMPORT_CHOICE_FILE && ( + <> + {showDelimiterChoice && ( + + )} + - )} -

-
-
- {importType === IMPORT_CHOICE_TEXT && ( - - )} - {importType === IMPORT_CHOICE_FILE && ( - - )} + + )} + {importType === IMPORT_CHOICE_FILE && ( + + Preview + + )} + + + Import + + + + {importType === IMPORT_CHOICE_TEXT && ( + + )} + {importType === IMPORT_CHOICE_FILE && ( +
+ {isLoadingPreview ? ( + + ) : ( + + )} +
+ )} +
+
diff --git a/src/plugins/data_importer/public/components/import_type_selector.tsx b/src/plugins/data_importer/public/components/import_type_selector.tsx index f24910e6d4ac..4cd86f3f53b9 100644 --- a/src/plugins/data_importer/public/components/import_type_selector.tsx +++ b/src/plugins/data_importer/public/components/import_type_selector.tsx @@ -61,35 +61,39 @@ export const ImportTypeSelector = ({ ), }} > - onChange(IMPORT_CHOICE_FILE)} - /> + + + onChange(IMPORT_CHOICE_FILE)} + /> + - - - onChange(IMPORT_CHOICE_TEXT)} - /> + + onChange(IMPORT_CHOICE_TEXT)} + /> + + diff --git a/src/plugins/data_importer/public/components/preview_table.scss b/src/plugins/data_importer/public/components/preview_table.scss new file mode 100644 index 000000000000..f984d04fd706 --- /dev/null +++ b/src/plugins/data_importer/public/components/preview_table.scss @@ -0,0 +1,6 @@ +// stylelint-disable-next-line @osd/stylelint/no_modifying_global_selectors +.customSearchBar .euiFormControlLayoutIcons { + height: 20px; + display: flex; + align-items: center; +} diff --git a/src/plugins/data_importer/public/components/preview_table.tsx b/src/plugins/data_importer/public/components/preview_table.tsx new file mode 100644 index 000000000000..8cb41518a140 --- /dev/null +++ b/src/plugins/data_importer/public/components/preview_table.tsx @@ -0,0 +1,127 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiText, + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, + EuiButton, + EuiFieldSearch, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; +import './preview_table.scss'; + +interface PreviewComponentProps { + previewData: any[]; + visibleRows: number; + loadMoreRows: () => void; + predictedMapping: Record; + existingMapping: Record; +} + +export const PreviewComponent = ({ + previewData, + visibleRows, + loadMoreRows, + predictedMapping, + existingMapping, +}: PreviewComponentProps) => { + const [searchQuery, setSearchQuery] = useState(''); + const totalRows = previewData?.length; + const loadedRows = Math.min(visibleRows, totalRows); + + const filteredData = previewData?.filter((row) => + Object.values(row).some( + (value) => + typeof value === 'string' && value.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); + + const getCellStyle = (field: string) => { + const predictedType = predictedMapping?.properties?.[field]?.type; + const existingType = existingMapping?.properties?.[field]?.type; + if (predictedType && existingType && predictedType !== existingType) { + return { color: '#BD271E' }; + } + return {}; + }; + + const getTooltipContent = (field: string) => { + const predictedType = predictedMapping?.properties?.[field]?.type; + const existingType = existingMapping?.properties?.[field]?.type; + if (predictedType && existingType && predictedType !== existingType) { + return `Predicted type: ${predictedType}, Existing type: ${existingType}`; + } + return ''; + }; + + return ( + <> +
+ +

+ Preview Data ({loadedRows}/{totalRows}) +

+
+ setSearchQuery(e.target.value)} + isClearable + className="customSearchBar" + /> +
+
+ + + # + {previewData?.length > 0 ? ( + Object.keys(previewData[0]).map((key) => ( + {key} + )) + ) : ( + Column + )} + + + {totalRows > 0 && + filteredData?.slice(0, loadedRows).map((row, rowIndex) => ( + + {rowIndex + 1} + {Object.keys(row).map((field, colIndex) => ( + + {row[field]} + {getTooltipContent(field) && ( + + + + )} + + ))} + + ))} + + + + {totalRows === 0 && ( + +

No data to display. Please upload a file to see the preview.

+
+ )} +
+ {loadedRows < totalRows && ( + + Click to See More + + )} + + ); +}; diff --git a/src/plugins/data_importer/server/routes/cat_indices.ts b/src/plugins/data_importer/server/routes/cat_indices.ts index ce2d3b5477bd..b1d7768b6789 100644 --- a/src/plugins/data_importer/server/routes/cat_indices.ts +++ b/src/plugins/data_importer/server/routes/cat_indices.ts @@ -14,7 +14,7 @@ export function catIndicesRoute( config: TypeOf, dataSourceEnabled: boolean ) { - router.post( + router.get( { path: '/api/data_importer/_cat_indices', validate: { @@ -24,7 +24,11 @@ export function catIndicesRoute( }, }, async (context, request, response) => { - const client = await decideClient(dataSourceEnabled, context, request.query.dataSource); + const client = await decideClient( + dataSourceEnabled, + context, + (request.query as { dataSource?: string }).dataSource + ); if (!!!client) { return response.notFound({ body: 'Data source is not enabled or does not exist', diff --git a/src/plugins/data_importer/server/routes/import_file.ts b/src/plugins/data_importer/server/routes/import_file.ts index ff495a47b68e..1e2597a206ea 100644 --- a/src/plugins/data_importer/server/routes/import_file.ts +++ b/src/plugins/data_importer/server/routes/import_file.ts @@ -87,10 +87,11 @@ export function importFileRoute( } } else { try { + const mapping = request.body.mapping ? JSON.parse(request.body.mapping) : {}; await client.indices.create({ index: request.query.indexName, body: { - mappings: JSON.parse(request.body.mapping!), + mappings: mapping, }, }); } catch (e) { diff --git a/yarn.lock b/yarn.lock index 4a87818db3a4..8bb7fba70a59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1527,6 +1527,29 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== +"@fast-csv/format@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-5.0.2.tgz#4799c3d3badd7b5d4ed596635a5cf094dd9501a9" + integrity sha512-fRYcWvI8vs0Zxa/8fXd/QlmQYWWkJqKZPAXM+vksnplb3owQFKTPPh9JqOtD0L3flQw/AZjjXdPkD7Kp/uHm8g== + dependencies: + lodash.escaperegexp "^4.1.2" + lodash.isboolean "^3.0.3" + lodash.isequal "^4.5.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + +"@fast-csv/parse@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-5.0.2.tgz#204000dfd661b580a10a8cd035a0e986fc6954a9" + integrity sha512-gMu1Btmm99TP+wc0tZnlH30E/F1Gw1Tah3oMDBHNPe9W8S68ixVHjt89Wg5lh7d9RuQMtwN+sGl5kxR891+fzw== + dependencies: + lodash.escaperegexp "^4.1.2" + lodash.groupby "^4.6.0" + lodash.isfunction "^3.0.9" + lodash.isnil "^4.0.0" + lodash.isundefined "^3.0.1" + lodash.uniq "^4.5.0" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -3534,6 +3557,14 @@ dependencies: "@types/node" "*" +"@types/ndjson@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/ndjson/-/ndjson-2.0.4.tgz#c5ad9d1baec99ec5582885a444cd7493d28a1987" + integrity sha512-ajAl7AjhFstF6waORYNSS49GL5iBKisqJlgvXuprXFKCX9fto4ordlNU3+XMgkMddgeR0WoQQBmKUk0v0dJ4pw== + dependencies: + "@types/node" "*" + "@types/through" "*" + "@types/node-forge@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.0.1.tgz#0df103639da9d5ec6a708d462020f0df70679f37" @@ -8430,6 +8461,14 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== +fast-csv@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-5.0.2.tgz#2676b7514ff1feb3de0f1c20d49015bae3285c83" + integrity sha512-CnB2zYAzzeh5Ta0UhSf32NexLy2SsEsSMY+fMWPV40k1OgaLEbm9Hf5dms3z/9fASZHBjB6i834079gVeksEqQ== + dependencies: + "@fast-csv/format" "5.0.2" + "@fast-csv/parse" "5.0.2" + fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -11919,6 +11958,11 @@ lodash.escape@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== + lodash.find@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" @@ -11939,16 +11983,41 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.groupby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" + integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + lodash.isequal@^4.0.0, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + +lodash.isnil@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" + integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng== + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= +lodash.isundefined@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" + integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA== + lodash.max@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.max/-/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a" @@ -11989,6 +12058,11 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + lodash@4.17.21, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -12789,6 +12863,17 @@ ncp@^2.0.0: resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= +ndjson@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19" + integrity sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ== + dependencies: + json-stringify-safe "^5.0.1" + minimist "^1.2.5" + readable-stream "^3.6.0" + split2 "^3.0.0" + through2 "^4.0.0" + nearley@^2.7.10: version "2.20.1" resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" @@ -14624,7 +14709,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -15906,6 +15991,13 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@1.1.2, sprintf-js@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" @@ -16738,6 +16830,13 @@ through2@^3.0.1: inherits "^2.0.4" readable-stream "2 || 3" +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"