From 79fbc6aa75fd2a76afa612ffca644e5c915cd991 Mon Sep 17 00:00:00 2001 From: Andrew Sikowitz Date: Wed, 19 Feb 2025 10:50:13 -0800 Subject: [PATCH] feat(ui,graphql): Finish bringing alchemy UI to OSS (#12670) --- .../dataset/mappers/DatasetProfileMapper.java | 33 +++ .../src/main/resources/entity.graphql | 39 +++ .../mappers/DatasetProfileMapperTest.java | 103 ++++++- datahub-web-react/package.json | 2 + datahub-web-react/src/Mocks.tsx | 2 +- .../components/BarChart/BarChart.stories.tsx | 106 ++++++-- .../components/BarChart/BarChart.tsx | 253 ++++++++++-------- .../components/BarChart/components.tsx | 10 - .../components/LeftAxisMarginSetter.tsx | 57 ++++ .../BarChart/components/TruncatableTick.tsx | 22 ++ .../components/BarChart/constants.ts | 41 +++ .../components/BarChart/defaults.ts | 48 ++++ .../hooks/useAdaptYAccessorToZeroValues.ts | 18 -- .../hooks/useAdaptYScaleToZeroValues.ts | 23 -- .../BarChart/hooks/useMaxDataValue.ts | 4 +- .../BarChart/hooks/useMinDataValue.ts | 6 + .../BarChart/hooks/usePrepareAccessors.ts | 31 +++ .../BarChart/hooks/usePrepareScales.ts | 35 +++ .../components/BarChart/types.ts | 70 +++-- .../components/BarChart/utils.ts | 16 +- .../components/Button/utils.ts | 4 + .../CalendarChart/CalendarChart.stories.tsx | 3 + .../CalendarChart/CalendarChart.tsx | 9 +- .../private/components/AxisBottomMonths.tsx | 2 +- .../private/components/AxisLeftWeekdays.tsx | 4 +- .../components/CalendarChart/types.ts | 2 + .../components/Drawer/Drawer.stories.tsx | 3 +- .../components/Drawer/Drawer.tsx | 29 +- .../components/Drawer/components.tsx | 4 + .../components/Drawer/defaults.ts | 7 + .../components/Drawer/types.ts | 1 + .../components/Editor/Editor.stories.tsx | 93 +++++++ .../components/Editor/Editor.tsx | 115 ++++++++ .../components/Editor/EditorTheme.tsx | 134 ++++++++++ .../components/Editor/OnChangeMarkdown.tsx | 24 ++ .../Editor/extensions/htmlToMarkdown.tsx | 117 ++++++++ .../Editor/extensions/markdownToHtml.tsx | 27 ++ .../mentions/DataHubMentionsExtension.tsx | 138 ++++++++++ .../extensions/mentions/MentionsComponent.tsx | 68 +++++ .../extensions/mentions/MentionsDropdown.tsx | 114 ++++++++ .../extensions/mentions/MentionsNodeView.tsx | 69 +++++ .../extensions/mentions/useDataHubMentions.ts | 63 +++++ .../Editor/toolbar/AddImageButton.tsx | 57 ++++ .../Editor/toolbar/AddLinkButton.tsx | 31 +++ .../Editor/toolbar/CodeBlockToolbar.tsx | 85 ++++++ .../Editor/toolbar/CommandButton.tsx | 28 ++ .../Editor/toolbar/FloatingToolbar.tsx | 118 ++++++++ .../components/Editor/toolbar/HeadingMenu.tsx | 69 +++++ .../components/Editor/toolbar/Icons.tsx | 45 ++++ .../components/Editor/toolbar/LinkModal.tsx | 82 ++++++ .../Editor/toolbar/TableCellMenu.tsx | 71 +++++ .../components/Editor/toolbar/Toolbar.tsx | 111 ++++++++ .../components/Editor/utils.ts | 7 + .../components/GraphCard/components.tsx | 1 + .../components/IconLabel/IconLabel.tsx | 2 +- .../IconLabel/{component.ts => components.ts} | 2 +- .../IncidentPriorityLabel.tsx | 53 ++-- .../IncidentPriorityLabel/components.ts | 7 + .../LineChart/LineChart.stories.tsx | 24 +- .../components/LineChart/LineChart.tsx | 200 ++++++-------- .../components/LineChart/components.tsx | 30 ++- .../components/LineChart/customTooltip.css | 4 + .../components/LineChart/defaults.tsx | 78 ++++++ .../components/LineChart/types.ts | 31 ++- .../components/Pills/Pill.tsx | 2 + .../components/Select/Nested/NestedSelect.tsx | 10 +- .../SelectItemsPopover/SelectItems.tsx | 4 +- .../components/Table/Table.tsx | 3 +- .../components/Text/Text.tsx | 4 +- .../components/Text/components.ts | 4 + .../components/Text/types.ts | 2 +- .../WhiskerChart/WhiskerChart.stories.tsx | 114 ++++++++ .../components/WhiskerChart/WhiskerChart.tsx | 142 ++++++++++ .../components/GlyphWithLineAndPopover.tsx | 75 ++++++ .../WhiskerChart/components/MetricPoint.tsx | 29 ++ .../components/WhiskerRenderer.tsx | 136 ++++++++++ .../components/WhiskerChart/constants.ts | 37 +++ .../components/WhiskerChart/defaults.tsx | 12 + .../components/WhiskerChart/index.ts | 1 + .../components/WhiskerChart/types.ts | 68 +++++ .../components/WhiskerChart/utils.ts | 21 ++ .../components/dataviz/utils.ts | 10 +- .../src/alchemy-components/index.ts | 1 + datahub-web-react/src/app/DataHubTitle.tsx | 39 +++ datahub-web-react/src/app/ProtectedRoutes.tsx | 4 +- datahub-web-react/src/app/SearchRoutes.tsx | 4 +- .../app/entity/mlFeature/MLFeatureEntity.tsx | 9 + .../src/app/entity/mlModel/MLModelEntity.tsx | 9 + .../StructuredProperty/RichTextInput.tsx | 2 + .../sidebar/SidebarSiblingsSection.tsx | 5 +- .../UrnInput/useUrnInput.tsx | 11 +- .../src/app/entityV2/chart/ChartEntity.tsx | 4 +- .../entityV2/container/ContainerEntity.tsx | 4 +- .../entityV2/dashboard/DashboardEntity.tsx | 4 +- .../app/entityV2/dataFlow/DataFlowEntity.tsx | 4 +- .../app/entityV2/dataJob/DataJobEntity.tsx | 4 +- .../DataProcessInstanceEntity.tsx | 6 +- .../dataProduct/DataProductEntity.tsx | 4 +- .../app/entityV2/dataset/DatasetEntity.tsx | 2 +- .../src/app/entityV2/domain/DomainEntity.tsx | 4 +- .../glossaryNode/GlossaryNodeEntity.tsx | 4 +- .../glossaryTerm/GlossaryTermEntity.tsx | 4 +- .../entityV2/mlFeature/MLFeatureEntity.tsx | 17 +- .../mlFeatureTable/MLFeatureTableEntity.tsx | 4 +- .../app/entityV2/mlModel/MLModelEntity.tsx | 17 +- .../mlModelGroup/MLModelGroupEntity.tsx | 4 +- .../mlPrimaryKey/MLPrimaryKeyEntity.tsx | 4 +- .../ownership/table/ActionsColumn.tsx | 17 +- .../EntityDropdown/EntityMenuActions.tsx | 2 +- .../src/app/entityV2/shared/GroupBySelect.tsx | 39 +++ ...ryLoading.tsx => TableLoadingSkeleton.tsx} | 4 +- .../components/search/InlineListSearch.tsx | 45 ++++ .../styledComponents.tsx} | 52 +--- .../src/app/entityV2/shared/constants.ts | 2 + .../profile/sidebar/Lineage/utils.tsx | 4 +- .../profile/sidebar/SidebarLogicSection.tsx | 11 + .../sidebar/SidebarSiblingsSection.tsx | 18 +- .../SidebarStructuredProperties.tsx | 15 +- .../SchemaFieldDrawer/SchemaFieldDrawer.tsx | 7 +- .../SchemaFieldDrawer/StatsSidebarView.tsx | 8 +- .../Validations/AcrylValidationsTab.tsx | 2 +- .../AssertionList/AcrylAssertionList.tsx | 6 +- .../AcrylAssertionListFilters.tsx | 13 +- .../Summary/AcrylAssertionSummarySection.tsx | 9 +- .../tabs/Dataset/Validations/acrylUtils.tsx | 3 +- .../assertion/profile/actions/ActionItem.tsx | 3 + .../shared/tabs/Properties/PropertiesTab.tsx | 55 ++-- .../Properties/StructuredPropertyValue.tsx | 24 +- .../shared/tabs/Properties/ValuesColumn.tsx | 33 ++- .../tabs/Properties/useHydratedEntityMap.ts | 26 ++ .../Properties/useStructuredProperties.tsx | 1 + .../src/app/entityV2/shared/utils.ts | 33 +++ .../insight/cards/PopularGlossaryTerms.tsx | 2 +- .../src/app/ingest/ManageIngestionPage.tsx | 11 +- .../executions/IngestionExecutionTable.tsx | 17 +- .../IngestionExecutionTableColumns.tsx | 62 ++++- .../PreviewCardFooterRightSection.tsx | 6 +- .../component/CompactEntityNameComponent.tsx | 87 ++++++ .../component/CompactEntityNameList.tsx | 88 ++---- .../renderer/component/EntityPreviewTag.tsx | 16 +- .../filters/value/EntityValueMenu.tsx | 6 +- .../src/app/searchV2/filters/value/utils.tsx | 22 +- .../src/app/shared/button/styledComponents.ts | 8 + .../src/app/shared/formatNumber.ts | 6 +- datahub-web-react/src/app/shared/textUtil.ts | 4 +- .../src/app/shared/time/timeUtils.tsx | 10 +- datahub-web-react/src/graphql/dataset.graphql | 8 + .../src/graphql/fragments.graphql | 23 ++ datahub-web-react/src/graphql/search.graphql | 10 +- datahub-web-react/vite.config.ts | 6 +- datahub-web-react/yarn.lock | 50 +++- 151 files changed, 4128 insertions(+), 679 deletions(-) create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/components/LeftAxisMarginSetter.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/components/TruncatableTick.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/constants.ts create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/defaults.ts delete mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYAccessorToZeroValues.ts delete mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYScaleToZeroValues.ts create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMinDataValue.ts create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareAccessors.ts create mode 100644 datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareScales.ts create mode 100644 datahub-web-react/src/alchemy-components/components/Drawer/defaults.ts create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/Editor.stories.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/Editor.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/EditorTheme.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/OnChangeMarkdown.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/htmlToMarkdown.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/markdownToHtml.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/mentions/DataHubMentionsExtension.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/mentions/MentionsComponent.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/mentions/MentionsDropdown.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/mentions/MentionsNodeView.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/extensions/mentions/useDataHubMentions.ts create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/AddImageButton.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/AddLinkButton.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/CodeBlockToolbar.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/CommandButton.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/FloatingToolbar.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/HeadingMenu.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/Icons.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/LinkModal.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/TableCellMenu.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/toolbar/Toolbar.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/Editor/utils.ts rename datahub-web-react/src/alchemy-components/components/IconLabel/{component.ts => components.ts} (96%) create mode 100644 datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/components.ts create mode 100644 datahub-web-react/src/alchemy-components/components/LineChart/customTooltip.css create mode 100644 datahub-web-react/src/alchemy-components/components/LineChart/defaults.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.stories.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/components/GlyphWithLineAndPopover.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/components/MetricPoint.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/components/WhiskerRenderer.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/constants.ts create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/defaults.tsx create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/index.ts create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/types.ts create mode 100644 datahub-web-react/src/alchemy-components/components/WhiskerChart/utils.ts create mode 100644 datahub-web-react/src/app/DataHubTitle.tsx create mode 100644 datahub-web-react/src/app/entityV2/shared/GroupBySelect.tsx rename datahub-web-react/src/app/entityV2/shared/{tabs/Dataset/Validations/AcrylAssertionsSummaryLoading.tsx => TableLoadingSkeleton.tsx} (92%) create mode 100644 datahub-web-react/src/app/entityV2/shared/components/search/InlineListSearch.tsx rename datahub-web-react/src/app/entityV2/shared/components/{ListSearch/AcrylListSearch.tsx => search/styledComponents.tsx} (52%) create mode 100644 datahub-web-react/src/app/entityV2/shared/tabs/Properties/useHydratedEntityMap.ts create mode 100644 datahub-web-react/src/app/recommendations/renderer/component/CompactEntityNameComponent.tsx create mode 100644 datahub-web-react/src/app/shared/button/styledComponents.ts diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapper.java index e966993871d06..14581a762fa1e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapper.java @@ -4,6 +4,8 @@ import com.linkedin.datahub.graphql.types.mappers.TimeSeriesAspectMapper; import com.linkedin.dataset.DatasetFieldProfile; import com.linkedin.dataset.DatasetProfile; +import com.linkedin.dataset.Quantile; +import com.linkedin.dataset.ValueFrequency; import com.linkedin.metadata.aspect.EnvelopedAspect; import com.linkedin.metadata.utils.GenericRecordUtils; import java.util.stream.Collectors; @@ -66,6 +68,37 @@ private static com.linkedin.datahub.graphql.generated.DatasetFieldProfile mapFie result.setNullProportion(gmsProfile.getNullProportion()); } result.setSampleValues(gmsProfile.getSampleValues()); + if (gmsProfile.hasQuantiles()) { + result.setQuantiles( + gmsProfile.getQuantiles().stream() + .map(DatasetProfileMapper::mapQuantile) + .collect(Collectors.toList())); + } + if (gmsProfile.hasDistinctValueFrequencies()) { + result.setDistinctValueFrequencies( + gmsProfile.getDistinctValueFrequencies().stream() + .map(DatasetProfileMapper::mapValueFrequency) + .collect(Collectors.toList())); + } + return result; + } + + private static com.linkedin.datahub.graphql.generated.Quantile mapQuantile(Quantile quantile) { + final com.linkedin.datahub.graphql.generated.Quantile result = + new com.linkedin.datahub.graphql.generated.Quantile(); + result.setQuantile(quantile.getQuantile()); + result.setValue(quantile.getValue()); + + return result; + } + + private static com.linkedin.datahub.graphql.generated.ValueFrequency mapValueFrequency( + ValueFrequency frequencies) { + final com.linkedin.datahub.graphql.generated.ValueFrequency result = + new com.linkedin.datahub.graphql.generated.ValueFrequency(); + result.setValue(frequencies.getValue()); + result.setFrequency(frequencies.getFrequency()); + return result; } } diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 25615d9b6aa4f..96b1dc544d204 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -7411,6 +7411,34 @@ type DatasetProfile implements TimeSeriesAspect { partitionSpec: PartitionSpec } +""" +A quantile along with its corresponding value +""" +type Quantile { + """ + Quantile. E.g. "0.25" for the 25th percentile + """ + quantile: String! + """ + The value of the quantile + """ + value: String! +} + +""" +A frequency distribution of a specific value within a dataset +""" +type ValueFrequency { + """ + Specific value. For numeric colums, the value will contain a strigified value + """ + value: String! + """ + Volume of the value + """ + frequency: Long! +} + """ An individual Dataset Field Profile """ @@ -7469,6 +7497,17 @@ type DatasetFieldProfile { A set of sample values for the field """ sampleValues: [String!] + + """ + Sorted list of quantile cutoffs for the field, in ascending order + Only for numerical columns + """ + quantiles: [Quantile!] + + """ + Volume of each column value for a low-cardinality / categorical field + """ + distinctValueFrequencies: [ValueFrequency!] } """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapperTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapperTest.java index 42220091f5853..e760fc72139df 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapperTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetProfileMapperTest.java @@ -1,10 +1,15 @@ package com.linkedin.datahub.graphql.types.dataset.mappers; import com.google.common.collect.ImmutableList; +import com.linkedin.data.template.SetMode; import com.linkedin.data.template.StringArray; import com.linkedin.datahub.graphql.generated.DatasetProfile; import com.linkedin.dataset.DatasetFieldProfile; import com.linkedin.dataset.DatasetFieldProfileArray; +import com.linkedin.dataset.Quantile; +import com.linkedin.dataset.QuantileArray; +import com.linkedin.dataset.ValueFrequency; +import com.linkedin.dataset.ValueFrequencyArray; import com.linkedin.metadata.aspect.EnvelopedAspect; import com.linkedin.metadata.utils.GenericRecordUtils; import java.util.ArrayList; @@ -19,6 +24,19 @@ public void testMapperFullProfile() { input.setRowCount(10L); input.setColumnCount(45L); input.setSizeInBytes(15L); + + ValueFrequency valueFrequency = new ValueFrequency(); + valueFrequency.setValue("2"); + valueFrequency.setFrequency(10L); + + Quantile quantile25 = new Quantile(); + quantile25.setQuantile("0.25"); + quantile25.setValue("1"); + + Quantile quantile75 = new Quantile(); + quantile75.setQuantile("0.75"); + quantile75.setValue("5"); + input.setFieldProfiles( new DatasetFieldProfileArray( ImmutableList.of( @@ -33,7 +51,11 @@ public void testMapperFullProfile() { .setNullProportion(20.5f) .setUniqueCount(30L) .setUniqueProportion(30.5f) - .setSampleValues(new StringArray(ImmutableList.of("val1", "val2"))), + .setSampleValues(new StringArray(ImmutableList.of("val1", "val2"))) + .setQuantiles(new QuantileArray(ImmutableList.of(quantile25, quantile75))) + .setDistinctValueFrequencies( + new ValueFrequencyArray(ImmutableList.of(valueFrequency)), + SetMode.IGNORE_NULL), new DatasetFieldProfile() .setFieldPath("/field2") .setMax("2") @@ -45,7 +67,11 @@ public void testMapperFullProfile() { .setNullProportion(30.5f) .setUniqueCount(40L) .setUniqueProportion(40.5f) - .setSampleValues(new StringArray(ImmutableList.of("val3", "val4")))))); + .setSampleValues(new StringArray(ImmutableList.of("val3", "val4"))) + .setQuantiles(new QuantileArray(ImmutableList.of(quantile25, quantile75))) + .setDistinctValueFrequencies( + new ValueFrequencyArray(ImmutableList.of(valueFrequency)), + SetMode.IGNORE_NULL)))); final EnvelopedAspect inputAspect = new EnvelopedAspect().setAspect(GenericRecordUtils.serializeAspect(input)); final DatasetProfile actual = DatasetProfileMapper.map(null, inputAspect); @@ -68,7 +94,14 @@ public void testMapperFullProfile() { "2", "4", "3", - new ArrayList<>(ImmutableList.of("val1", "val2"))), + new ArrayList<>(ImmutableList.of("val1", "val2")), + new ArrayList( + ImmutableList.of( + new com.linkedin.datahub.graphql.generated.Quantile("0.25", "1"), + new com.linkedin.datahub.graphql.generated.Quantile("0.75", "5"))), + new ArrayList( + ImmutableList.of( + new com.linkedin.datahub.graphql.generated.ValueFrequency("2", 10L)))), new com.linkedin.datahub.graphql.generated.DatasetFieldProfile( "/field2", 40L, @@ -80,7 +113,15 @@ public void testMapperFullProfile() { "3", "5", "4", - new ArrayList<>(ImmutableList.of("val3", "val4")))))); + new ArrayList<>(ImmutableList.of("val3", "val4")), + new ArrayList( + ImmutableList.of( + new com.linkedin.datahub.graphql.generated.Quantile("0.25", "1"), + new com.linkedin.datahub.graphql.generated.Quantile("0.75", "5"))), + new ArrayList( + ImmutableList.of( + new com.linkedin.datahub.graphql.generated.ValueFrequency( + "2", 10L))))))); Assert.assertEquals(actual.getTimestampMillis(), expected.getTimestampMillis()); Assert.assertEquals(actual.getRowCount(), expected.getRowCount()); Assert.assertEquals(actual.getColumnCount(), expected.getColumnCount()); @@ -113,6 +154,24 @@ public void testMapperFullProfile() { Assert.assertEquals( actual.getFieldProfiles().get(0).getSampleValues(), expected.getFieldProfiles().get(0).getSampleValues()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles().get(0).getQuantile(), + expected.getFieldProfiles().get(0).getQuantiles().get(0).getQuantile()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles().get(0).getValue(), + expected.getFieldProfiles().get(0).getQuantiles().get(0).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles().get(1).getQuantile(), + expected.getFieldProfiles().get(0).getQuantiles().get(1).getQuantile()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles().get(1).getValue(), + expected.getFieldProfiles().get(0).getQuantiles().get(1).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getDistinctValueFrequencies().get(0).getValue(), + expected.getFieldProfiles().get(0).getDistinctValueFrequencies().get(0).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getDistinctValueFrequencies().get(0).getFrequency(), + expected.getFieldProfiles().get(0).getDistinctValueFrequencies().get(0).getFrequency()); Assert.assertEquals( actual.getFieldProfiles().get(1).getFieldPath(), @@ -141,6 +200,24 @@ public void testMapperFullProfile() { Assert.assertEquals( actual.getFieldProfiles().get(1).getSampleValues(), expected.getFieldProfiles().get(1).getSampleValues()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getQuantiles().get(0).getQuantile(), + expected.getFieldProfiles().get(1).getQuantiles().get(0).getQuantile()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles().get(0).getValue(), + expected.getFieldProfiles().get(0).getQuantiles().get(0).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getQuantiles().get(1).getQuantile(), + expected.getFieldProfiles().get(1).getQuantiles().get(1).getQuantile()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getQuantiles().get(1).getValue(), + expected.getFieldProfiles().get(1).getQuantiles().get(1).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getDistinctValueFrequencies().get(0).getValue(), + expected.getFieldProfiles().get(1).getDistinctValueFrequencies().get(0).getValue()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getDistinctValueFrequencies().get(0).getFrequency(), + expected.getFieldProfiles().get(1).getDistinctValueFrequencies().get(0).getFrequency()); } @Test @@ -176,9 +253,11 @@ public void testMapperPartialProfile() { new ArrayList<>( ImmutableList.of( new com.linkedin.datahub.graphql.generated.DatasetFieldProfile( - "/field1", 30L, 30.5f, null, null, null, null, null, null, null, null), + "/field1", 30L, 30.5f, null, null, null, null, null, null, null, null, null, + null), new com.linkedin.datahub.graphql.generated.DatasetFieldProfile( - "/field2", 40L, 40.5f, null, null, "6", "2", "3", "5", "4", null)))); + "/field2", 40L, 40.5f, null, null, "6", "2", "3", "5", "4", null, null, + null)))); Assert.assertEquals(actual.getTimestampMillis(), expected.getTimestampMillis()); Assert.assertEquals(actual.getRowCount(), expected.getRowCount()); Assert.assertEquals(actual.getColumnCount(), expected.getColumnCount()); @@ -211,6 +290,12 @@ public void testMapperPartialProfile() { Assert.assertEquals( actual.getFieldProfiles().get(0).getSampleValues(), expected.getFieldProfiles().get(0).getSampleValues()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getQuantiles(), + expected.getFieldProfiles().get(0).getQuantiles()); + Assert.assertEquals( + actual.getFieldProfiles().get(0).getDistinctValueFrequencies(), + expected.getFieldProfiles().get(0).getDistinctValueFrequencies()); Assert.assertEquals( actual.getFieldProfiles().get(1).getFieldPath(), @@ -239,5 +324,11 @@ public void testMapperPartialProfile() { Assert.assertEquals( actual.getFieldProfiles().get(1).getSampleValues(), expected.getFieldProfiles().get(1).getSampleValues()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getQuantiles(), + expected.getFieldProfiles().get(1).getQuantiles()); + Assert.assertEquals( + actual.getFieldProfiles().get(1).getDistinctValueFrequencies(), + expected.getFieldProfiles().get(1).getDistinctValueFrequencies()); } } diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json index 55cc8e0f50c44..01933de2d7a29 100644 --- a/datahub-web-react/package.json +++ b/datahub-web-react/package.json @@ -43,6 +43,8 @@ "@visx/marker": "^3.5.0", "@visx/scale": "^3.2.0", "@visx/shape": "^3.2.0", + "@visx/stats": "^3.12.0", + "@visx/tooltip": "^3.12.0", "@visx/xychart": "^3.2.0", "@visx/zoom": "^3.1.1", "analytics": "^0.8.9", diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index e3a5632c9f5fc..78b83b685da0b 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -1175,7 +1175,7 @@ const glossaryTerm2 = { __typename: 'GlossaryTerm', }; -const glossaryTerm3 = { +export const glossaryTerm3 = { urn: 'urn:li:glossaryTerm:example.glossaryterm2', type: 'GLOSSARY_TERM', name: 'glossaryterm2', diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx index f0ee44725ad57..887e08a498eec 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx +++ b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.stories.tsx @@ -2,9 +2,11 @@ import React from 'react'; import { BADGE } from '@geometricpanda/storybook-addon-badges'; import type { Meta, StoryObj } from '@storybook/react'; import { BarChart } from './BarChart'; -import { getMockedProps } from './utils'; -import { DEFAULT_MIN_VALUE } from './hooks/useAdaptYAccessorToZeroValues'; -import { DEFAULT_MAX_DOMAIN_VALUE } from './hooks/useAdaptYScaleToZeroValues'; +import { generateMockDataHorizontal, getMockedProps } from './utils'; +import { DEFAULT_MIN_VALUE } from './hooks/usePrepareAccessors'; +import { DEFAULT_MAX_DOMAIN_VALUE } from './hooks/usePrepareScales'; +import { abbreviateNumber } from '../dataviz/utils'; +import { DEFAULT_LENGTH_OF_LEFT_AXIS_LABEL } from './constants'; const meta = { title: 'Charts / BarChart', @@ -23,12 +25,26 @@ const meta = { argTypes: { data: { description: 'Array of datum to show', + table: { + type: { + summary: 'DatumType[]', + detail: + 'The DatumType includes:\n' + + '- x: A numeric value representing the x-coordinate.\n' + + '- y: A numeric value representing the y-coordinate.\n' + + '- colorScheme (optional): A ColorScheme enum value to define the color of the data point. ' + + 'If not provided, a default color may be used.', + }, + }, }, - xAccessor: { - description: 'A function to convert datum to value of X', - }, - yAccessor: { - description: 'A function to convert datum to value of Y', + horizontal: { + description: 'Whether to show horizontal bars', + table: { + defaultValue: { summary: 'false' }, + }, + control: { + type: 'boolean', + }, }, maxYDomainForZeroData: { description: @@ -50,20 +66,24 @@ const meta = { margin: { description: 'Add margins to chart', }, - barColor: { - description: 'Color of bar', - control: { - type: 'color', - }, + leftAxisProps: { + description: 'The props for the left axis', }, - barSelectedColor: { - description: 'Color of selected bar', + maxLengthOfLeftAxisLabel: { + description: + 'Enables truncating of label up to provided value. The full value will be available in popover', + table: { + defaultValue: { summary: `${DEFAULT_LENGTH_OF_LEFT_AXIS_LABEL}` }, + type: { + summary: 'number', + }, + }, control: { - type: 'color', + type: 'number', }, }, - leftAxisProps: { - description: 'The props for the left axis', + showLeftAxisLine: { + description: 'Enable to show left vertical line', }, bottomAxisProps: { description: 'The props for the bottom axis', @@ -71,9 +91,6 @@ const meta = { gridProps: { description: 'The props for the grid', }, - renderGradients: { - description: 'A function to render different gradients that can be used as colors', - }, }, // Define defaults @@ -95,3 +112,50 @@ export const sandbox: Story = { ), }; + +export const horizontal: Story = { + args: { + horizontal: true, + xScale: { + type: 'linear', + nice: true, + round: true, + }, + yScale: { + type: 'band', + reverse: true, + padding: 0.1, + }, + gridProps: { + rows: false, + columns: true, + strokeWidth: 1, + }, + margin: { + top: 0, + right: 20, + bottom: 0, + left: 20, + }, + + bottomAxisProps: { + tickFormat: (v) => abbreviateNumber(v), + }, + }, + render: (props) => { + const data = generateMockDataHorizontal(); + + return ( +
+ data.find((datum) => datum.y === y)?.label, + computeNumTicks: () => data.length, + }} + /> +
+ ); + }, +}; diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx index 96c2b4f3ebeae..5143e069991aa 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx +++ b/datahub-web-react/src/alchemy-components/components/BarChart/BarChart.tsx @@ -1,114 +1,133 @@ -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { colors } from '@src/alchemy-components/theme'; -import { abbreviateNumber } from '@src/app/dataviz/utils'; -import { TickLabelProps } from '@visx/axis'; import { LinearGradient } from '@visx/gradient'; import { ParentSize } from '@visx/responsive'; import { Axis, AxisScale, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; -import dayjs from 'dayjs'; import { Popover } from '../Popover'; import { ChartWrapper, StyledBarSeries } from './components'; -import { AxisProps, BarChartProps } from './types'; +import { AxisProps, BarChartProps, ColorAccessor, Datum, GridProps, XAccessor, YAccessor } from './types'; import { getMockedProps } from './utils'; import useMergedProps from './hooks/useMergedProps'; -import useAdaptYScaleToZeroValues from './hooks/useAdaptYScaleToZeroValues'; -import useAdaptYAccessorToZeroValue from './hooks/useAdaptYAccessorToZeroValues'; -import useMaxDataValue from './hooks/useMaxDataValue'; - -const commonTickLabelProps: TickLabelProps = { - fontSize: 10, - fontFamily: 'Mulish', - fill: colors.gray[1700], -}; - -export const barChartDefault: BarChartProps = { - data: [], - - xAccessor: (datum) => datum?.x, - yAccessor: (datum) => datum?.y, - xScale: { type: 'band', paddingInner: 0.4, paddingOuter: 0.1 }, - yScale: { type: 'linear', nice: true, round: true }, - - barColor: 'url(#bar-gradient)', - barSelectedColor: colors.violet[500], - - leftAxisProps: { - tickFormat: abbreviateNumber, - tickLabelProps: { - ...commonTickLabelProps, - textAnchor: 'end', - }, - hideAxisLine: true, - hideTicks: true, - }, - bottomAxisProps: { - tickFormat: (value) => dayjs(value).format('DD MMM'), - tickLabelProps: { - ...commonTickLabelProps, - textAnchor: 'middle', - verticalAnchor: 'start', - width: 20, - }, - hideAxisLine: true, - hideTicks: true, - }, - gridProps: { - rows: true, - columns: false, - stroke: '#e0e0e0', - strokeWidth: 1, - lineStyle: {}, - }, - - renderGradients: () => , -}; - -export function BarChart({ +import usePrepareScales from './hooks/usePrepareScales'; +import usePrepareAccessors from './hooks/usePrepareAccessors'; +import { COLOR_SCHEME_TO_PARAMS, DEFAULT_COLOR_SCHEME } from './constants'; +import TruncatableTick from './components/TruncatableTick'; +import { barChartDefault } from './defaults'; +import LeftAxisMarginSetter from './components/LeftAxisMarginSetter'; +import { abbreviateNumber } from '../dataviz/utils'; + +export function BarChart({ data, isEmpty, + horizontal, - xAccessor = barChartDefault.xAccessor, - yAccessor = barChartDefault.yAccessor, xScale = barChartDefault.xScale, yScale = barChartDefault.yScale, maxYDomainForZeroData, minYForZeroData, - barColor = barChartDefault.barColor, - barSelectedColor = barChartDefault.barSelectedColor, margin, leftAxisProps = barChartDefault.leftAxisProps, + maxLengthOfLeftAxisLabel = barChartDefault.maxLengthOfLeftAxisLabel, + showLeftAxisLine = barChartDefault.showLeftAxisLine, bottomAxisProps = barChartDefault.bottomAxisProps, gridProps = barChartDefault.gridProps, popoverRenderer, - renderGradients = barChartDefault.renderGradients, -}: BarChartProps) { - const [hasSelectedBar, setHasSelectedBar] = useState(false); +}: BarChartProps) { + const [selectedBarIndex, setSelectedBarIndex] = useState(null); + const [howeredBarIndex, setHoweredBarIndex] = useState(null); + const [leftAxisMargin, setLeftAxisMargin] = useState(0); // FYI: additional margins to show left and bottom axises - const internalMargin = { - top: (margin?.top ?? 0) + 30, - right: margin?.right ?? 0, - bottom: (margin?.bottom ?? 0) + 35, - left: (margin?.left ?? 0) + 40, - }; - - const maxDataValue = useMaxDataValue(data, yAccessor); - const adaptedYScale = useAdaptYScaleToZeroValues(yScale, maxDataValue, maxYDomainForZeroData); - const adaptedYAccessor = useAdaptYAccessorToZeroValue(yAccessor, maxDataValue, minYForZeroData); + const internalMargin = useMemo( + () => ({ + top: (margin?.top ?? 0) + 30, + right: (margin?.right ?? 0) + 0, + bottom: (margin?.bottom ?? 0) + 35, + left: (margin?.left ?? 0) + leftAxisMargin + 6, + }), + [leftAxisMargin, margin], + ); - const accessors = { xAccessor, yAccessor: adaptedYAccessor }; + const xAccessor: XAccessor = (datum) => datum.x; + const yAccessor: YAccessor = (datum) => datum.y; + const accessors = usePrepareAccessors(data, !!horizontal, xAccessor, yAccessor, minYForZeroData); + const scales = usePrepareScales(data, !!horizontal, xScale, xAccessor, yScale, yAccessor, maxYDomainForZeroData); - const { computeNumTicks: computeLeftAxisNumTicks, ...mergedLeftAxisProps } = useMergedProps>( + const { computeNumTicks: computeLeftAxisNumTicks, ...mergedLeftAxisProps } = useMergedProps( leftAxisProps, barChartDefault.leftAxisProps, ); - const { computeNumTicks: computeBottomAxisNumTicks, ...mergedBottomAxisProps } = useMergedProps< - AxisProps - >(bottomAxisProps, barChartDefault.bottomAxisProps); + const { computeNumTicks: computeBottomAxisNumTicks, ...mergedBottomAxisProps } = useMergedProps( + bottomAxisProps, + barChartDefault.bottomAxisProps, + ); + + const mergedGridProps = useMergedProps(gridProps, barChartDefault.gridProps); + + const gradientIdSuffix = useMemo(() => `bar${horizontal ? `-horizontal` : ''}`, [horizontal]); + + const colorAccessor: ColorAccessor = useCallback( + (datum, index) => { + if (isEmpty) return colors.transparent; + const colorTheme = datum.colorScheme ?? DEFAULT_COLOR_SCHEME; + const colorThemeParams = COLOR_SCHEME_TO_PARAMS[colorTheme]; + if (index === selectedBarIndex) return colorThemeParams.mainColor; + if (index === howeredBarIndex) return colorThemeParams.mainColor; + + const isInversed = (horizontal ? accessors.xAccessor(datum) : accessors.yAccessor(datum)) < 0; + + return `url(#${gradientIdSuffix}-${colorTheme}${isInversed ? '-inversed' : ''})`; + }, + [selectedBarIndex, howeredBarIndex, gradientIdSuffix, isEmpty, accessors, horizontal], + ); + + const renderGradients = () => { + const colorSchemes = [ + ...new Set([ + ...data.map((datum) => datum.colorScheme).filter((scheme) => scheme !== undefined), + DEFAULT_COLOR_SCHEME, + ]), + ]; + + return ( + <> + {colorSchemes.map((colorScheme) => { + const colorSchemeParams = COLOR_SCHEME_TO_PARAMS[colorScheme ?? DEFAULT_COLOR_SCHEME]; + const { mainColor } = colorSchemeParams; + const { alternativeColor } = colorSchemeParams; + const fromColor = horizontal ? alternativeColor : mainColor; + const toColor = horizontal ? mainColor : alternativeColor; + const gradientId = `${gradientIdSuffix}-${colorScheme}`; + const gradientInversedId = `${gradientId}-inversed`; + + return ( + <> + + + + ); + })} + + ); + }; // In case of no data we should render empty graph with axises // but they don't render at all without any data. @@ -125,18 +144,27 @@ export function BarChart({ - {renderGradients?.()} + {renderGradients()} ( + + )} {...mergedLeftAxisProps} /> + ({ {...mergedBottomAxisProps} /> - - - + + + {/* hide the first (left) column line */} + {mergedGridProps.columns && ( + + )} + + {showLeftAxisLine && ( + + )} } - $hasSelectedItem={hasSelectedBar} - $color={barColor} - $selectedColor={barSelectedColor} + as={BarSeries} + $hasSelectedItem={selectedBarIndex !== null} $isEmpty={isEmpty} dataKey="bar-seria-0" data={data} radius={4} radiusTop - onBlur={() => setHasSelectedBar(false)} - onFocus={() => setHasSelectedBar(true)} - // Internally the library doesn't emmit these events if handlers are empty - // They are requred to show/hide/move tooltip - onPointerMove={() => null} - onPointerUp={() => null} - onPointerOut={() => null} + radiusBottom={horizontal} + onBlur={() => setSelectedBarIndex(null)} + onFocus={({ index }) => setSelectedBarIndex(index)} + colorAccessor={colorAccessor} + onPointerMove={({ index }) => setHoweredBarIndex(index)} + onPointerOut={() => setHoweredBarIndex(null)} {...accessors} /> - + + // needed for bounds to update correctly (https://airbnb.io/visx/tooltip) + key={Math.random()} snapTooltipToDatumX snapTooltipToDatumY unstyled @@ -185,7 +226,9 @@ export function BarChart({ diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx index a0a3f7a2cebfc..8d9149284c3be 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx +++ b/datahub-web-react/src/alchemy-components/components/BarChart/components.tsx @@ -1,4 +1,3 @@ -import { colors } from '@src/alchemy-components/theme'; import { BarSeries } from '@visx/xychart'; import styled from 'styled-components'; @@ -10,8 +9,6 @@ export const ChartWrapper = styled.div` export const StyledBarSeries = styled(BarSeries)<{ $hasSelectedItem?: boolean; - $color?: string; - $selectedColor?: string; $isEmpty?: boolean; }>` & { @@ -19,21 +16,14 @@ export const StyledBarSeries = styled(BarSeries)<{ ${(props) => props.$isEmpty && 'pointer-events: none;'} - fill: ${(props) => { - if (props.$isEmpty) return colors.transparent; - return (props.$hasSelectedItem ? props.$selectedColor : props.$color) || colors.violet[500]; - }}; - ${(props) => props.$hasSelectedItem && 'opacity: 0.3;'} :hover { - fill: ${(props) => props.$selectedColor || colors.violet[500]}; filter: drop-shadow(0px -2px 5px rgba(33, 23, 95, 0.3)); opacity: 1; } :focus { - fill: ${(props) => props.$selectedColor || colors.violet[500]}; outline: none; opacity: 1; } diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/components/LeftAxisMarginSetter.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/components/LeftAxisMarginSetter.tsx new file mode 100644 index 0000000000000..8da6c30db3b5e --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/components/LeftAxisMarginSetter.tsx @@ -0,0 +1,57 @@ +import { DataContext } from '@visx/xychart'; +import { useCallback, useContext, useEffect } from 'react'; +import { getTicks } from '@visx/scale'; +import { TickFormatter } from '@visx/axis'; + +interface LeftAxisMarginSetterProps { + setLeftMargin?: (margin: number) => void; + numOfTicks?: number; + formatter: TickFormatter; + charWidth?: number; + maxMargin?: number; +} + +const DEFAULT_CHAR_WIDTH = 7.5; +const MINIMAL_LABEL_LENGTH = 1; + +/** + * FYI: to get real ticks (for approximate calculation of maximum label width) that will be shown on the axis, + * we should get the final scale object that available only in DataContext under XYChart + */ +export default function LeftAxisMarginSetter({ + setLeftMargin, + numOfTicks, + formatter, + charWidth, + maxMargin, +}: LeftAxisMarginSetterProps) { + const { yScale } = useContext(DataContext); + + const computeMargin = useCallback( + (ticks: number[]) => { + const maxLengthOfLabel = Math.max( + ...ticks + .map((tick, index) => ({ value: tick, index })) + .map( + (tick, index, items) => + formatter(tick.value, index, items)?.toString()?.length ?? MINIMAL_LABEL_LENGTH, + ), + ); + + const margin = Math.ceil(maxLengthOfLabel * (charWidth ?? DEFAULT_CHAR_WIDTH)); + + if (maxMargin) return Math.min(margin, maxMargin); + + return margin; + }, + [formatter, charWidth, maxMargin], + ); + + useEffect(() => { + if (yScale) { + setLeftMargin?.(computeMargin(getTicks(yScale, numOfTicks))); + } + }, [yScale, setLeftMargin, numOfTicks, computeMargin]); + + return null; +} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/components/TruncatableTick.tsx b/datahub-web-react/src/alchemy-components/components/BarChart/components/TruncatableTick.tsx new file mode 100644 index 0000000000000..526387eaddfcd --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/components/TruncatableTick.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Text } from '@visx/text'; +import { Popover } from '../../Popover'; +import { TruncatableTickProps } from '../types'; + +export default function TruncatableTick({ formattedValue, limit, ...textProps }: TruncatableTickProps) { + if (formattedValue === undefined) return null; + // FYI: formatted value has type `string | undefined` but when zero is ignored, formattedValue will have type Number + if (typeof formattedValue !== 'string') return null; + + const truncatedValue = formattedValue.slice(0, limit); + const isValueTruncated = formattedValue.length !== truncatedValue.length; + const finalValue = truncatedValue + (isValueTruncated ? '…' : ''); + + return ( + + + {finalValue} + + + ); +} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/constants.ts b/datahub-web-react/src/alchemy-components/components/BarChart/constants.ts new file mode 100644 index 0000000000000..2c3e0b11ef5b0 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/constants.ts @@ -0,0 +1,41 @@ +import { colors } from '@src/alchemy-components/theme'; +import { ColorScheme, ColorSchemeParams } from './types'; + +export const VIOLET_COLOR_SCHEME_PRARAMS: ColorSchemeParams = { + mainColor: colors.violet[500], + alternativeColor: '#917FFF', +}; + +export const BLUE_COLOR_SCHEME_PARAMS: ColorSchemeParams = { + mainColor: colors.blue[400], + alternativeColor: '#CCEBF6', +}; + +export const RED_COLOR_SCHEME_PARAMS: ColorSchemeParams = { + mainColor: colors.red[400], + alternativeColor: colors.red[200], +}; + +export const ORANGE_COLOR_SCHEME_PARAMS: ColorSchemeParams = { + mainColor: '#FFD8B1', + alternativeColor: '#FFF3E0', +}; + +export const GREEN_COLOR_SCHEME_PARAMS: ColorSchemeParams = { + mainColor: colors.green[400], + alternativeColor: colors.green[200], +}; + +export const COLOR_SCHEMES: ColorScheme[] = Object.values(ColorScheme); + +export const DEFAULT_COLOR_SCHEME = ColorScheme.Violet; + +export const COLOR_SCHEME_TO_PARAMS = { + [ColorScheme.Violet]: VIOLET_COLOR_SCHEME_PRARAMS, + [ColorScheme.Blue]: BLUE_COLOR_SCHEME_PARAMS, + [ColorScheme.Pink]: RED_COLOR_SCHEME_PARAMS, + [ColorScheme.Orange]: ORANGE_COLOR_SCHEME_PARAMS, + [ColorScheme.Green]: GREEN_COLOR_SCHEME_PARAMS, +}; + +export const DEFAULT_LENGTH_OF_LEFT_AXIS_LABEL = 7; diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/defaults.ts b/datahub-web-react/src/alchemy-components/components/BarChart/defaults.ts new file mode 100644 index 0000000000000..9f8c44f10d2b1 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/defaults.ts @@ -0,0 +1,48 @@ +import { colors } from '@src/alchemy-components/theme'; +import { TickLabelProps } from '@visx/axis'; +import dayjs from 'dayjs'; +import { DEFAULT_LENGTH_OF_LEFT_AXIS_LABEL } from './constants'; +import { BarChartProps, Datum } from './types'; +import { abbreviateNumber } from '../dataviz/utils'; + +const commonTickLabelProps: TickLabelProps = { + fontSize: 10, + fontFamily: 'Mulish', + fill: colors.gray[1700], +}; + +export const barChartDefault: Partial = { + xScale: { type: 'band', paddingInner: 0.4, paddingOuter: 0.1 }, + yScale: { type: 'linear', nice: true, round: true }, + + leftAxisProps: { + tickFormat: abbreviateNumber, + tickLabelProps: { + ...commonTickLabelProps, + textAnchor: 'end', + width: 50, + }, + hideAxisLine: true, + hideTicks: true, + }, + maxLengthOfLeftAxisLabel: DEFAULT_LENGTH_OF_LEFT_AXIS_LABEL, + showLeftAxisLine: false, + bottomAxisProps: { + tickFormat: (value) => dayjs(value).format('DD MMM'), + tickLabelProps: { + ...commonTickLabelProps, + textAnchor: 'middle', + verticalAnchor: 'start', + width: 20, + }, + hideAxisLine: true, + hideTicks: true, + }, + gridProps: { + rows: true, + columns: false, + stroke: '#e0e0e0', + strokeWidth: 1, + lineStyle: {}, + }, +}; diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYAccessorToZeroValues.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYAccessorToZeroValues.ts deleted file mode 100644 index 19831af296262..0000000000000 --- a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYAccessorToZeroValues.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMemo } from 'react'; -import { YAccessor } from '../types'; - -export const DEFAULT_MIN_VALUE = 0.1; - -export default function useAdaptYAccessorToZeroValue( - yAccessor: YAccessor, - maxDataValue: number, - minimalValue: number | undefined, -): YAccessor { - return useMemo(() => { - // Data contains non zero values, skip adaptation - if (maxDataValue > 0) return yAccessor; - - // add minimal `y` value - return (value) => Math.max(yAccessor(value), minimalValue ?? DEFAULT_MIN_VALUE); - }, [yAccessor, maxDataValue, minimalValue]); -} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYScaleToZeroValues.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYScaleToZeroValues.ts deleted file mode 100644 index f46dd7297f309..0000000000000 --- a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useAdaptYScaleToZeroValues.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AxisScaleOutput } from '@visx/axis'; -import { ScaleConfig } from '@visx/scale'; -import { useMemo } from 'react'; - -export const DEFAULT_MAX_DOMAIN_VALUE = 10; - -export default function useAdaptYScaleToZeroValues( - yScale: ScaleConfig | undefined, - maxDataValue: number, - maxDomainValue: number | undefined, -): ScaleConfig | undefined { - return useMemo(() => { - // yScale should be passed for adaptation otherwise return it as is - if (!yScale) return yScale; - - // Data contains non zero values, no need to adapt - if (maxDataValue > 0) return yScale; - - // Add domain with max value to show data with only zero values correctly - const domain: [number, number] = [0, maxDomainValue ?? DEFAULT_MAX_DOMAIN_VALUE]; - return { domain, ...yScale }; - }, [maxDataValue, maxDomainValue, yScale]); -} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMaxDataValue.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMaxDataValue.ts index 331756a1bca89..a8448fa8f5d9b 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMaxDataValue.ts +++ b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMaxDataValue.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { YAccessor } from '../types'; +import { BaseDatum, YAccessor } from '../types'; -export default function useMaxDataValue(data: T[], yAccessor: YAccessor): number { +export default function useMaxDataValue(data: BaseDatum[], yAccessor: YAccessor): number { return useMemo(() => Math.max(...data.map(yAccessor)) ?? 0, [data, yAccessor]); } diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMinDataValue.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMinDataValue.ts new file mode 100644 index 0000000000000..287baf45aa50f --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/useMinDataValue.ts @@ -0,0 +1,6 @@ +import { useMemo } from 'react'; +import { BaseDatum, YAccessor } from '../types'; + +export default function useMinDataValue(data: BaseDatum[], yAccessor: YAccessor): number { + return useMemo(() => Math.min(...data.map(yAccessor)) ?? 0, [data, yAccessor]); +} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareAccessors.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareAccessors.ts new file mode 100644 index 0000000000000..9166cd2f592d9 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareAccessors.ts @@ -0,0 +1,31 @@ +import { useCallback, useMemo } from 'react'; +import { BaseDatum, XAccessor, YAccessor } from '../types'; + +export const DEFAULT_MIN_VALUE = 0.1; + +export default function usePrepareAccessors( + data: BaseDatum[], + horizontal: boolean, + xAccessor: XAccessor, + yAccessor: YAccessor, + minimalValue?: number, +) { + const setMinimalValueForZeroData = useCallback( + (accessor: XAccessor | YAccessor) => { + const hasNonZeroValues = data.filter((datum) => accessor(datum) !== 0).length > 0; + if (hasNonZeroValues) return accessor; + return (value: BaseDatum) => Math.max(accessor(value), minimalValue ?? DEFAULT_MIN_VALUE); + }, + [data, minimalValue], + ); + + const accessors = useMemo( + () => ({ + xAccessor: horizontal ? setMinimalValueForZeroData(xAccessor) : xAccessor, + yAccessor: horizontal ? yAccessor : setMinimalValueForZeroData(yAccessor), + }), + [yAccessor, xAccessor, horizontal, setMinimalValueForZeroData], + ); + + return accessors; +} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareScales.ts b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareScales.ts new file mode 100644 index 0000000000000..2579d30ab9382 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/BarChart/hooks/usePrepareScales.ts @@ -0,0 +1,35 @@ +import { useCallback, useMemo } from 'react'; +import { BaseDatum, Scale, XAccessor, YAccessor } from '../types'; + +export const DEFAULT_MAX_DOMAIN_VALUE = 10; + +export default function usePrepareScales( + data: BaseDatum[], + horizontal: boolean, + xScale: Scale | undefined, + xAccessorOriginal: XAccessor, + yScale: Scale | undefined, + yAccessorOriginal: YAccessor, + maxDomainValue: number | undefined, +) { + const setDomainForZeroData = useCallback( + (scale: Scale | undefined, accessor: XAccessor | YAccessor) => { + if (!scale) return scale; + const hasNonZeroValues = data.filter((datum) => accessor(datum) !== 0).length > 0; + if (hasNonZeroValues) return scale; + const domain: [number, number] = [0, maxDomainValue ?? DEFAULT_MAX_DOMAIN_VALUE]; + return { domain, ...scale }; + }, + [data, maxDomainValue], + ); + + const scales = useMemo( + () => ({ + xScale: horizontal ? setDomainForZeroData(xScale, xAccessorOriginal) : xScale, + yScale: horizontal ? yScale : setDomainForZeroData(yScale, yAccessorOriginal), + }), + [yScale, yAccessorOriginal, xScale, xAccessorOriginal, horizontal, setDomainForZeroData], + ); + + return scales; +} diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/types.ts b/datahub-web-react/src/alchemy-components/components/BarChart/types.ts index 26e216ef2c2fc..584206d92328e 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/types.ts +++ b/datahub-web-react/src/alchemy-components/components/BarChart/types.ts @@ -1,38 +1,68 @@ -import { AxisScaleOutput } from '@visx/axis'; +import { AxisScaleOutput, TickRendererProps } from '@visx/axis'; import { ScaleConfig } from '@visx/scale'; import { Margin } from '@visx/xychart'; import { AxisProps as VisxAxisProps } from '@visx/xychart/lib/components/axis/Axis'; import { GridProps as VisxGridProps } from '@visx/xychart/lib/components/grid/Grid'; -export type AxisProps = Omit & { - computeNumTicks?: (width: number, height: number, margin: Margin, data: DatumType[]) => number | undefined; +export enum ColorScheme { + Violet = 'VIOLET', + Blue = 'BLUE', + Pink = 'PINK', + Orange = 'ORANGE', + Green = 'GREEN', +} + +export interface BaseDatum { + x: number; + y: number; +} + +export type Datum = BaseDatum & { + colorScheme?: ColorScheme; }; -export type GridProps = Omit & { - computeNumTicks?: (width: number, height: number, margin: Margin, data: DatumType[]) => number | undefined; +export type AxisProps = Omit & { + computeNumTicks?: (width: number, height: number, margin: Margin, data: BaseDatum[]) => number | undefined; }; -export type YAccessor = (datum: T) => number; +export type GridProps = Omit & { + computeNumTicks?: (width: number, height: number, margin: Margin, data: BaseDatum[]) => number | undefined; +}; + +export type ValueAccessor = (datum: BaseDatum) => number; +export type YAccessor = ValueAccessor; +export type XAccessor = ValueAccessor; + +export type ColorAccessor = (datum: Datum, index: number) => string; -export type BarChartProps = { - data: DatumType[]; +export type Scale = ScaleConfig; + +export type BarChartProps = { + data: Datum[]; isEmpty?: boolean; + horizontal?: boolean; - xAccessor: (datum: DatumType) => string | number; - yAccessor: YAccessor; - xScale?: ScaleConfig; - yScale?: ScaleConfig; + xScale?: Scale; + yScale?: Scale; maxYDomainForZeroData?: number; minYForZeroData?: number; - barColor?: string; - barSelectedColor?: string; - margin?: Margin; + margin?: Partial; + + leftAxisProps?: AxisProps; + showLeftAxisLine?: boolean; + maxLengthOfLeftAxisLabel?: number; + bottomAxisProps?: AxisProps; + gridProps?: GridProps; - leftAxisProps?: AxisProps; - bottomAxisProps?: AxisProps; - gridProps?: GridProps; + popoverRenderer?: (datum: Datum) => React.ReactNode; +}; + +export type TruncatableTickProps = TickRendererProps & { + limit?: number; +}; - popoverRenderer?: (datum: DatumType) => React.ReactNode; - renderGradients?: () => React.ReactNode; +export type ColorSchemeParams = { + mainColor: string; + alternativeColor: string; }; diff --git a/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts b/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts index 48f100a281300..107bcbeea8778 100644 --- a/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts +++ b/datahub-web-react/src/alchemy-components/components/BarChart/utils.ts @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import { COLOR_SCHEMES } from './constants'; export function generateMockData(length = 30, maxValue = 50_000, minValue = 0) { return Array(length) @@ -18,9 +19,22 @@ export function generateMockData(length = 30, maxValue = 50_000, minValue = 0) { }); } +export function generateMockDataHorizontal(length = 5, maxValue = 50_000, minValue = 0) { + return Array(length) + .fill(0) + .map((_, index) => { + return { + y: index, + x: Math.max(Math.random() * maxValue, minValue), + colorScheme: COLOR_SCHEMES?.[index % (COLOR_SCHEMES.length - 1)], + label: `Value-${index}${' text'.repeat(index)}`, + }; + }); +} + export function getMockedProps() { return { - data: generateMockData(), + data: generateMockData(5), xAccessor: (datum) => datum.x, yAccessor: (datum) => Math.max(datum.y, 1000), }; diff --git a/datahub-web-react/src/alchemy-components/components/Button/utils.ts b/datahub-web-react/src/alchemy-components/components/Button/utils.ts index 6eacf232b0373..a0825dadce831 100644 --- a/datahub-web-react/src/alchemy-components/components/Button/utils.ts +++ b/datahub-web-react/src/alchemy-components/components/Button/utils.ts @@ -184,6 +184,10 @@ const getButtonPadding = (size: SizeOptions, variant: ButtonVariant, isCircle: b if (isCircle) return { padding: spacing.xsm }; const paddingStyles = { + xs: { + vertical: 0, + horizontal: 0, + }, sm: { vertical: 8, horizontal: 12, diff --git a/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.stories.tsx index 1221da45be815..1f05b9eb3edcd 100644 --- a/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.stories.tsx +++ b/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.stories.tsx @@ -46,6 +46,9 @@ const meta = { leftAxisLabelProps: { description: 'Props for label of left axis', }, + showLeftAxisLine: { + description: 'Enable to show left vertical line', + }, bottomAxisLabelProps: { description: 'Props for label of bottom axis', }, diff --git a/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.tsx b/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.tsx index ca51e63b6feb8..59f853903a575 100644 --- a/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.tsx +++ b/datahub-web-react/src/alchemy-components/components/CalendarChart/CalendarChart.tsx @@ -22,9 +22,10 @@ export const calendarChartDefault: Omit, 'colorAccessor' ...commonLabelProps, textAnchor: 'end', }, + showLeftAxisLine: false, bottomAxisLabelProps: { ...commonLabelProps, - textAnchor: 'middle', + textAnchor: 'start', }, maxHeight: 350, showPopover: true, @@ -38,6 +39,7 @@ export function CalendarChart({ showPopover = calendarChartDefault.showPopover, popoverRenderer, leftAxisLabelProps = calendarChartDefault.leftAxisLabelProps, + showLeftAxisLine = calendarChartDefault.showLeftAxisLine, bottomAxisLabelProps = calendarChartDefault.bottomAxisLabelProps, margin, maxHeight = calendarChartDefault.maxHeight, @@ -66,7 +68,10 @@ export function CalendarChart({ onDayClick={onDayClick} > - labelProps={leftAxisLabelProps} /> + + labelProps={leftAxisLabelProps} + showLeftAxisLine={showLeftAxisLine} + /> labelProps={bottomAxisLabelProps} /> data={preparedData} /> diff --git a/datahub-web-react/src/alchemy-components/components/CalendarChart/private/components/AxisBottomMonths.tsx b/datahub-web-react/src/alchemy-components/components/CalendarChart/private/components/AxisBottomMonths.tsx index eabbf12c45511..3b39b2d971793 100644 --- a/datahub-web-react/src/alchemy-components/components/CalendarChart/private/components/AxisBottomMonths.tsx +++ b/datahub-web-react/src/alchemy-components/components/CalendarChart/private/components/AxisBottomMonths.tsx @@ -17,7 +17,7 @@ export function AxisBottomMonths({ labelProps }: AxisBottomMonthsProp const weeksBefore = weeksInMonth.slice(0, monthIndex).reduce((acc, value) => acc + value, 0); const yLabel = DAYS_IN_WEEK * (squareSize + squareGap) + margin.top + axisTopMargin; - const xLabel = weeksBefore * (squareSize + squareGap) + squareGap * monthIndex + margin.left + 10; + const xLabel = weeksBefore * (squareSize + squareGap) + squareGap * monthIndex + margin.left; return ( ({ labelProps }: AxisLeftWeekdaysProps) { +export function AxisLeftWeekdays({ labelProps, showLeftAxisLine }: AxisLeftWeekdaysProps) { const { margin, squareSize, squareGap } = useCalendarState(); const yLineOffset = 5; @@ -29,7 +29,7 @@ export function AxisLeftWeekdays({ labelProps }: AxisLeftWeekdaysProp return ( <> {WEEKDAYS.map((weekday, index) => renderTickLabel(index, weekday))} - + {showLeftAxisLine && } ); } diff --git a/datahub-web-react/src/alchemy-components/components/CalendarChart/types.ts b/datahub-web-react/src/alchemy-components/components/CalendarChart/types.ts index 198775527c3cf..e1096c6b72dfa 100644 --- a/datahub-web-react/src/alchemy-components/components/CalendarChart/types.ts +++ b/datahub-web-react/src/alchemy-components/components/CalendarChart/types.ts @@ -40,6 +40,7 @@ export type CalendarChartProps = { showPopover?: boolean; popoverRenderer?: (day: DayData) => React.ReactNode; leftAxisLabelProps?: LabelProps; + showLeftAxisLine?: boolean; bottomAxisLabelProps?: LabelProps; margin?: Margin; maxHeight?: number; @@ -70,6 +71,7 @@ export type DayProps = { export type AxisLeftWeekdaysProps = { labelProps?: LabelProps; + showLeftAxisLine?: boolean; }; export type AxisBottomMonthsProps = { diff --git a/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.stories.tsx b/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.stories.tsx index df0014ae7824e..a295068cae428 100644 --- a/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.stories.tsx +++ b/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.stories.tsx @@ -2,8 +2,9 @@ import { BADGE } from '@geometricpanda/storybook-addon-badges'; import type { Meta, StoryObj } from '@storybook/react'; import React, { useState } from 'react'; import { Button } from '../Button'; -import { Drawer, drawerDefault } from './Drawer'; +import { Drawer } from './Drawer'; import { DrawerProps } from './types'; +import { drawerDefault } from './defaults'; // Auto Docs const meta = { diff --git a/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.tsx b/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.tsx index 65ec4a14286ea..1067b95700576 100644 --- a/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.tsx +++ b/datahub-web-react/src/alchemy-components/components/Drawer/Drawer.tsx @@ -1,21 +1,17 @@ import React from 'react'; import { Button } from '../Button'; import { Text } from '../Text'; -import { StyledDrawer, TitleContainer } from './components'; +import { StyledDrawer, TitleContainer, TitleLeftContainer } from './components'; import { maskTransparentStyle } from './constants'; import { DrawerProps } from './types'; - -export const drawerDefault: Omit = { - width: 600, - closable: true, - maskTransparent: false, -}; +import { drawerDefault } from './defaults'; export const Drawer = ({ title, children, open, onClose, + onBack, width = drawerDefault.width, closable = drawerDefault.closable, maskTransparent = drawerDefault.maskTransparent, @@ -26,9 +22,22 @@ export const Drawer = ({ destroyOnClose title={ - - {title} - + + {onBack && ( + + + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/FloatingToolbar.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/FloatingToolbar.tsx new file mode 100644 index 0000000000000..61bc3dafe9445 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/FloatingToolbar.tsx @@ -0,0 +1,118 @@ +import React, { useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { Typography } from 'antd'; +import { + BoldOutlined, + DisconnectOutlined, + EditOutlined, + ItalicOutlined, + LinkOutlined, + UnderlineOutlined, +} from '@ant-design/icons'; +import { FloatingWrapper, useActive, useAttrs, useCommands } from '@remirror/react'; +import { createMarkPositioner } from 'remirror/extensions'; +import { ANTD_GRAY } from '@src/app/entityV2/shared/constants'; + +import { CommandButton } from './CommandButton'; +import { LinkModal } from './LinkModal'; +import { CodeIcon } from './Icons'; + +const { Text } = Typography; + +export const ToolbarContainer = styled.span` + display: flex; + align-items: center; + padding: 2px; + background-color: ${ANTD_GRAY[1]}; + border-radius: 4px; + box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d; + overflow: hidden; + z-index: 300; +`; + +const LinkText = styled(Text)` + padding-left: 4px; + max-width: 250px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +export const FloatingToolbar = () => { + const [isModalVisible, setModalVisible] = useState(false); + const commands = useCommands(); + const active = useActive(true); + + const linkPositioner = useMemo(() => createMarkPositioner({ type: 'link' }), []); + const href = (useAttrs().link()?.href as string) ?? ''; + + const handleEditLink = () => { + setModalVisible(true); + }; + + const handleClose = () => setModalVisible(false); + + const linkCommmands = ( + + {href} + } commandName="editLink" onClick={handleEditLink} /> + } + commandName="toggleLink" + onClick={() => commands.removeLink()} + /> + + ); + + const shouldShowFloatingToolbar = !(active.link() || active.codeBlock()); + + return ( + <> + + {linkCommmands} + + {shouldShowFloatingToolbar && ( + + + } + commandName="toggleBold" + active={active.bold()} + onClick={() => commands.toggleBold()} + /> + } + commandName="toggleItalic" + active={active.italic()} + onClick={() => commands.toggleItalic()} + /> + } + commandName="toggleUnderline" + active={active.underline()} + onClick={() => commands.toggleUnderline()} + /> + } + commandName="updateLink" + onClick={handleEditLink} + /> + } + commandName="toggleCode" + active={active.code()} + onClick={() => commands.toggleCode()} + /> + + + )} + + + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/HeadingMenu.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/HeadingMenu.tsx new file mode 100644 index 0000000000000..4437a9ed85849 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/HeadingMenu.tsx @@ -0,0 +1,69 @@ +import React, { MouseEventHandler, useCallback } from 'react'; +import { Select } from 'antd'; +import styled from 'styled-components'; +import { useActive, useCommands } from '@remirror/react'; +import { ANTD_GRAY } from '@src/app/entityV2/shared/constants'; + +const { Option } = Select; + +const OPTIONS = [ + { tag: 'h1', label: 'Heading 1', value: 1 }, + { tag: 'h2', label: 'Heading 2', value: 2 }, + { tag: 'h3', label: 'Heading 3', value: 3 }, + { tag: 'h4', label: 'Heading 4', value: 4 }, + { tag: 'h5', label: 'Heading 5', value: 5 }, + { tag: 'p', label: 'Normal', value: 0 }, +]; + +/* To mitigate overrides of the Select's width when using it in modals */ +const Wrapper = styled.div` + display: inline-block; + width: 120px; + border: 1px solid ${ANTD_GRAY[4.5]}; + border-radius: 8px; +`; + +const StyledSelect = styled(Select)` + font-weight: 500; + width: 100%; +`; + +export const HeadingMenu = () => { + const { toggleHeading } = useCommands(); + const active = useActive(true); + + const activeHeading = OPTIONS.map(({ value }) => value).filter((level) => active.heading({ level }))?.[0] || 0; + + const handleMouseDown: MouseEventHandler = useCallback((e) => { + e.preventDefault(); + }, []); + + return ( + + { + const level = +`${value}`; + if (level) { + toggleHeading({ level }); + } else { + toggleHeading(); + } + }} + onMouseDown={handleMouseDown} + > + {OPTIONS.map((option) => { + return ( + + ); + })} + + + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Icons.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Icons.tsx new file mode 100644 index 0000000000000..b7954fd7b63c8 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Icons.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Icon from '@ant-design/icons'; +import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; + +export const CodeIcon = (props: Partial) => ( + ( + + + + )} + /> +); + +export const CodeBlockIcon = (props: Partial) => ( + ( + + + + )} + /> +); diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/LinkModal.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/LinkModal.tsx new file mode 100644 index 0000000000000..14698291575c4 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/LinkModal.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react'; +import { Form, Input, Modal, Typography } from 'antd'; +import { FromToProps } from '@remirror/core-types'; +import { useAttrs, useCommands, useEditorState, useHelpers } from '@remirror/react'; +import { getMarkRange } from '@remirror/core-utils'; + +type LinkModalProps = { + visible: boolean; + handleClose: () => void; +}; + +export const LinkModal = (props: LinkModalProps) => { + const { visible, handleClose } = props; + + const [trPos, setTrPos] = useState({ from: 0, to: 0 }); + const [form] = Form.useForm(); + + const commands = useCommands(); + const helpers = useHelpers(); + const editorState = useEditorState(); + + const href = (useAttrs().link()?.href as string) ?? ''; + + useEffect(() => { + if (visible) { + const { from, to } = editorState.selection; + const pos = getMarkRange(editorState.doc.resolve(from), 'link') || { from, to }; + + form.setFieldsValue({ + href, + text: helpers.getTextBetween(pos.from, pos.to, editorState.doc) || '', + }); + + setTrPos(pos); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible]); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + const displayText = values.text || values.href; + + commands.replaceText({ + content: displayText, + selection: trPos, + type: 'link', + attrs: { + href: values.href, + }, + }); + + form.resetFields(); + handleClose(); + } catch (e) { + console.log('Validate Failed:', e); + } + }; + + return ( + +
e.key === 'Enter' && form.submit()} + > + Link URL} + rules={[{ required: true }]} + > + + + Text}> + + +
+
+ ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/TableCellMenu.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/TableCellMenu.tsx new file mode 100644 index 0000000000000..7460a3327612b --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/TableCellMenu.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Dropdown, Menu } from 'antd'; +import styled from 'styled-components'; +import { useActive, useCommands } from '@remirror/react'; +import { DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons'; + +const StyledDropdownButton = styled(Dropdown.Button)` + position: absolute; + right: 2px; + top: 50%; + transform: translateY(-50%); + .ant-btn { + height: auto; + padding: 0; + &.ant-btn.ant-btn-icon-only { + width: 16px; + height: 16px; + border-radius: 5px; + } + } +`; + +export const TableCellMenu = () => { + const active = useActive(); + const commands = useCommands(); + + const menu = ( + + } + disabled={active.tableHeaderCell()} + onClick={() => commands.addTableRowBefore()} + > + Insert row above + + } onClick={() => commands.addTableRowAfter()}> + Insert row below + + } onClick={() => commands.addTableColumnBefore()}> + Insert column left + + } onClick={() => commands.addTableColumnAfter()}> + Insert column right + + + } + disabled={active.tableHeaderCell()} + onClick={() => commands.deleteTableRow()} + > + Delete row + + } onClick={() => commands.deleteTableColumn()}> + Delete column + + } onClick={() => commands.deleteTable()}> + Delete table + + + ); + + return ( + } + placement="bottomLeft" + overlay={menu} + type="primary" + /> + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Toolbar.tsx b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Toolbar.tsx new file mode 100644 index 0000000000000..bbefbdbc6fb43 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/toolbar/Toolbar.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Divider } from 'antd'; +import { useActive, useCommands } from '@remirror/react'; +import styled from 'styled-components'; +import { + Code, + CodeBlock, + ListBullets, + ListNumbers, + Table, + TextB, + TextItalic, + TextStrikethrough, + TextUnderline, +} from '@phosphor-icons/react'; +import colors from '@src/alchemy-components/theme/foundations/colors'; +import { CommandButton } from './CommandButton'; +import { HeadingMenu } from './HeadingMenu'; +import { AddImageButton } from './AddImageButton'; +import { AddLinkButton } from './AddLinkButton'; + +const Container = styled.div` + position: sticky; + top: 0; + z-index: 99; + background-color: white; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + padding: 8px !important; + & button { + line-height: 0; + } + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 6px -4px rgba(0, 0, 0, 0.1); +`; + +export const Toolbar = () => { + const commands = useCommands(); + const active = useActive(true); + + return ( + + + + } + style={{ marginRight: 2 }} + commandName="toggleBold" + active={active.bold()} + onClick={() => commands.toggleBold()} + /> + } + style={{ marginRight: 2 }} + commandName="toggleItalic" + active={active.italic()} + onClick={() => commands.toggleItalic()} + /> + } + style={{ marginRight: 2 }} + commandName="toggleUnderline" + active={active.underline()} + onClick={() => commands.toggleUnderline()} + /> + } + commandName="toggleStrike" + active={active.strike()} + onClick={() => commands.toggleStrike()} + /> + + } + commandName="toggleBulletList" + active={active.bulletList()} + onClick={() => commands.toggleBulletList()} + /> + } + commandName="toggleOrderedList" + active={active.orderedList()} + onClick={() => commands.toggleOrderedList()} + /> + + } + commandName="toggleCode" + active={active.code()} + onClick={() => commands.toggleCode()} + /> + } + commandName="toggleCodeBlock" + active={active.codeBlock()} + onClick={() => commands.toggleCodeBlock()} + /> + + + + } + commandName="createTable" + onClick={() => commands.createTable()} + disabled={active.table()} /* Disables nested tables */ + /> + + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/Editor/utils.ts b/datahub-web-react/src/alchemy-components/components/Editor/utils.ts new file mode 100644 index 0000000000000..8bbc0a92221f0 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/Editor/utils.ts @@ -0,0 +1,7 @@ +import DOMPurify from 'dompurify'; + +export const sanitizeRichText = (content: string) => { + if (!content) return ''; + const encoded = content.replace(//g, '>'); + return DOMPurify.sanitize(encoded).replace(/</g, '<').replace(/>/g, '>'); +}; diff --git a/datahub-web-react/src/alchemy-components/components/GraphCard/components.tsx b/datahub-web-react/src/alchemy-components/components/GraphCard/components.tsx index 025bb896df2f7..aa463ba15f3a0 100644 --- a/datahub-web-react/src/alchemy-components/components/GraphCard/components.tsx +++ b/datahub-web-react/src/alchemy-components/components/GraphCard/components.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; export const GraphCardHeader = styled.div` display: flex; flex-direction: row; + gap: 16px; justify-content: space-between; `; diff --git a/datahub-web-react/src/alchemy-components/components/IconLabel/IconLabel.tsx b/datahub-web-react/src/alchemy-components/components/IconLabel/IconLabel.tsx index 2fcf046c925c4..d1323af6d5fb8 100644 --- a/datahub-web-react/src/alchemy-components/components/IconLabel/IconLabel.tsx +++ b/datahub-web-react/src/alchemy-components/components/IconLabel/IconLabel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { IconLabelProps, IconType } from './types'; -import { IconLabelContainer, ImageContainer, Label } from './component'; +import { IconLabelContainer, ImageContainer, Label } from './components'; import { isValidImageUrl } from './utils'; export const IconLabel = ({ icon, name, type, style, imageUrl }: IconLabelProps) => { diff --git a/datahub-web-react/src/alchemy-components/components/IconLabel/component.ts b/datahub-web-react/src/alchemy-components/components/IconLabel/components.ts similarity index 96% rename from datahub-web-react/src/alchemy-components/components/IconLabel/component.ts rename to datahub-web-react/src/alchemy-components/components/IconLabel/components.ts index a2878cdbf3038..6abffb80dc63f 100644 --- a/datahub-web-react/src/alchemy-components/components/IconLabel/component.ts +++ b/datahub-web-react/src/alchemy-components/components/IconLabel/components.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const IconLabelContainer = styled.div` display: flex; align-items: center; - gap: 10px; + gap: 8px; `; export const ImageContainer = styled.div` diff --git a/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/IncidentPriorityLabel.tsx b/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/IncidentPriorityLabel.tsx index d3e0ba6ab9316..b5ee845b0546f 100644 --- a/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/IncidentPriorityLabel.tsx +++ b/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/IncidentPriorityLabel.tsx @@ -1,43 +1,38 @@ import React from 'react'; -import { ExclamationMark } from '@phosphor-icons/react'; -import colors from '@src/alchemy-components/theme/foundations/colors'; + +import CriticalIcon from '@src/images/incident-critical.svg'; +import HighIcon from '@src/images/incident-chart-bar-three.svg'; +import MediumIcon from '@src/images/incident-chart-bar-two.svg'; +import LowIcon from '@src/images/incident-chart-bar-one.svg'; +import { Label, StyledImage } from './components'; import { IconLabel } from '../IconLabel'; import { IncidentPriorityLabelProps } from './types'; -import { Bar } from '../Bar'; import { PRIORITIES } from './constant'; import { IconType } from '../IconLabel/types'; -const PRIORITY_LEVEL = { - [PRIORITIES.HIGH]: 3, - [PRIORITIES.MEDIUM]: 2, - [PRIORITIES.LOW]: 1, -}; - -const renderBars = (priority: string) => { - return ; +// 🔄 Map priorities to icons for cleaner code +const priorityIcons = { + [PRIORITIES.CRITICAL]: CriticalIcon, + [PRIORITIES.HIGH]: HighIcon, + [PRIORITIES.MEDIUM]: MediumIcon, + [PRIORITIES.LOW]: LowIcon, + [PRIORITIES.NONE]: null, }; -const Icons = { - [PRIORITIES.CRITICAL]: { - icon: , - type: IconType.ICON, - }, - [PRIORITIES.HIGH]: { - icon: renderBars(PRIORITIES.HIGH), - type: IconType.ICON, - }, - [PRIORITIES.MEDIUM]: { - icon: renderBars(PRIORITIES.MEDIUM), - type: IconType.ICON, - }, - [PRIORITIES.LOW]: { - icon: renderBars(PRIORITIES.LOW), - type: IconType.ICON, - }, -}; +// 🚀 Dynamically generate the Icons object +const Icons = Object.fromEntries( + Object.entries(priorityIcons).map(([priority, iconSrc]) => [ + priority, + { + icon: iconSrc ? : null, + type: IconType.ICON, + }, + ]), +); export const IncidentPriorityLabel = ({ priority, title, style }: IncidentPriorityLabelProps) => { const { icon, type } = Icons[priority] || {}; + if (!icon) return ; return ; }; diff --git a/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/components.ts b/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/components.ts new file mode 100644 index 0000000000000..d51d6ae662ee1 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/IncidentPriorityLabel/components.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const StyledImage = styled.img` + cursor: pointer; +`; + +export const Label = styled.span``; diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx index fce3921ff1ecd..260786cf766a2 100644 --- a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx +++ b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.stories.tsx @@ -3,7 +3,7 @@ import { BADGE } from '@geometricpanda/storybook-addon-badges'; import type { Meta, StoryObj } from '@storybook/react'; import { LineChart } from './LineChart'; import { getMockedProps } from '../BarChart/utils'; -import { DEFAULT_MAX_DOMAIN_VALUE } from '../BarChart/hooks/useAdaptYScaleToZeroValues'; +import { DEFAULT_MAX_DOMAIN_VALUE } from '../BarChart/hooks/usePrepareScales'; const meta = { title: 'Charts / LineChart', @@ -23,12 +23,6 @@ const meta = { data: { description: 'Array of datum to show', }, - xAccessor: { - description: 'A function to convert datum to value of X', - }, - yAccessor: { - description: 'A function to convert datum to value of Y', - }, maxYDomainForZeroData: { description: 'For the case where the data has only zero values, you can set the yScale domain to better display the chart', @@ -57,9 +51,15 @@ const meta = { leftAxisProps: { description: 'The props for the left axis', }, + showLeftAxisLine: { + description: 'Enable to show left vertical line', + }, bottomAxisProps: { description: 'The props for the bottom axis', }, + showBottomAxisLine: { + description: 'Enable to show bottom horizontal line', + }, gridProps: { description: 'The props for the grid', }, @@ -72,12 +72,22 @@ const meta = { renderTooltipGlyph: { description: 'A function to render a glyph', }, + showGlyphOnSingleDataPoint: { + description: 'Whether to show the glyph when there is only one data point', + control: { + type: 'boolean', + }, + }, + renderGlyphOnSingleDataPoint: { + description: 'A function to render a glyph for a single data point', + }, }, // Define defaults args: { ...getMockedProps(), popoverRenderer: (datum) => <>DATUM: {JSON.stringify(datum)}, + yScale: { type: 'linear', round: true, clamp: true }, }, } satisfies Meta; diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx index 540b798f1ab1d..f4571fedc6414 100644 --- a/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx +++ b/datahub-web-react/src/alchemy-components/components/LineChart/LineChart.tsx @@ -1,95 +1,30 @@ -import { colors } from '@src/alchemy-components/theme'; import { abbreviateNumber } from '@src/app/dataviz/utils'; -import { TickLabelProps } from '@visx/axis'; import { curveMonotoneX } from '@visx/curve'; -import { LinearGradient } from '@visx/gradient'; import { ParentSize } from '@visx/responsive'; -import { AreaSeries, Axis, AxisScale, Grid, Tooltip, XYChart } from '@visx/xychart'; -import dayjs from 'dayjs'; -import React, { useState } from 'react'; -import { Popover } from '../Popover'; -import { ChartWrapper, TooltipGlyph } from './components'; -import { LineChartProps } from './types'; -import { getMockedProps } from '../BarChart/utils'; +import { AreaSeries, Axis, AxisScale, GlyphSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; +import React, { useMemo, useState } from 'react'; +import usePrepareScales from '../BarChart/hooks/usePrepareScales'; import useMergedProps from '../BarChart/hooks/useMergedProps'; -import { roundToEven } from './utils'; import { AxisProps, GridProps } from '../BarChart/types'; -import { GLYPH_DROP_SHADOW_FILTER } from './constants'; -import useAdaptYScaleToZeroValues from '../BarChart/hooks/useAdaptYScaleToZeroValues'; -import useMaxDataValue from '../BarChart/hooks/useMaxDataValue'; - -const commonTickLabelProps: TickLabelProps = { - fontSize: 10, - fontFamily: 'Mulish', - fill: colors.gray[1700], -}; - -export const lineChartDefault: LineChartProps = { - data: [], - isEmpty: false, - - xAccessor: (datum) => datum?.x, - yAccessor: (datum) => datum?.y, - xScale: { type: 'time' }, - yScale: { type: 'log', nice: true, round: true, base: 2 }, - - lineColor: colors.violet[500], - areaColor: 'url(#line-gradient)', - margin: { top: 0, right: 0, bottom: 0, left: 0 }, - - leftAxisProps: { - tickFormat: abbreviateNumber, - tickLabelProps: { - ...commonTickLabelProps, - textAnchor: 'end', - width: 50, - }, - computeNumTicks: () => 5, - hideAxisLine: true, - hideTicks: true, - }, - bottomAxisProps: { - tickFormat: (x) => dayjs(x).format('D MMM'), - tickLabelProps: { - ...commonTickLabelProps, - textAnchor: 'middle', - verticalAnchor: 'start', - }, - computeNumTicks: (width, _, margin, data) => { - const widthOfTick = 80; - const widthOfAxis = width - margin.right - margin.left; - const maxCountOfTicks = Math.ceil(widthOfAxis / widthOfTick); - const numOfTicks = roundToEven(maxCountOfTicks / 2); - return Math.min(numOfTicks, data.length - 1); - }, - hideAxisLine: true, - hideTicks: true, - }, - gridProps: { - rows: true, - columns: false, - stroke: '#e0e0e0', - computeNumTicks: () => 5, - lineStyle: {}, - }, - - renderGradients: () => ( - - ), - toolbarVerticalCrosshairStyle: { - stroke: colors.white, - strokeWidth: 2, - filter: GLYPH_DROP_SHADOW_FILTER, - }, - renderTooltipGlyph: (props) => , -}; - -export function LineChart({ +import { getMockedProps } from '../BarChart/utils'; +import { Popover } from '../Popover'; +import { ChartWrapper } from './components'; +import { Datum, LineChartProps } from './types'; +// FIY: tooltip has a bug when glyph and vertical/horizontal crosshair can be shown behind the graph +// issue: https://github.com/airbnb/visx/issues/1333 +// We have this problem when LineChart shown on Drawer +// That can be fixed by adding z-idex +// But there are no ways to do it with StyledComponents as glyph and crosshairs rendered in portals +// https://github.com/styled-components/styled-components/issues/2620 +import LeftAxisMarginSetter from '../BarChart/components/LeftAxisMarginSetter'; +import './customTooltip.css'; +import { lineChartDefault } from './defaults'; +import useMinDataValue from '../BarChart/hooks/useMinDataValue'; + +export function LineChart({ data, isEmpty, - xAccessor = lineChartDefault.xAccessor, - yAccessor = lineChartDefault.yAccessor, xScale = lineChartDefault.xScale, yScale = lineChartDefault.yScale, maxYDomainForZeroData, @@ -99,43 +34,54 @@ export function LineChart({ margin, leftAxisProps, + showLeftAxisLine = lineChartDefault.showLeftAxisLine, bottomAxisProps, + showBottomAxisLine = lineChartDefault.showBottomAxisLine, gridProps, popoverRenderer, renderGradients = lineChartDefault.renderGradients, toolbarVerticalCrosshairStyle = lineChartDefault.toolbarVerticalCrosshairStyle, renderTooltipGlyph = lineChartDefault.renderTooltipGlyph, -}: LineChartProps) { + showGlyphOnSingleDataPoint = lineChartDefault.showGlyphOnSingleDataPoint, + renderGlyphOnSingleDataPoint = lineChartDefault.renderGlyphOnSingleDataPoint, +}: LineChartProps) { const [showGrid, setShowGrid] = useState(false); + const [leftAxisMargin, setLeftAxisMargin] = useState(0); // FYI: additional margins to show left and bottom axises - const internalMargin = { - top: (margin?.top ?? 0) + 30, - right: (margin?.right ?? 0) + 30, - bottom: (margin?.bottom ?? 0) + 35, - left: (margin?.left ?? 0) + 40, - }; - - const maxDataValue = useMaxDataValue(data, yAccessor); - const adaptedYScale = useAdaptYScaleToZeroValues(yScale, maxDataValue, maxYDomainForZeroData); + const internalMargin = useMemo( + () => ({ + top: (margin?.top ?? 0) + 30, + right: (margin?.right ?? 0) + 30, + bottom: (margin?.bottom ?? 0) + 35, + left: (margin?.left ?? 0) + leftAxisMargin + 6, + }), + [leftAxisMargin, margin], + ); + const xAccessor = (datum: Datum) => datum?.x; + const yAccessor = (datum: Datum) => datum.y; const accessors = { xAccessor, yAccessor }; + const scales = usePrepareScales(data, false, xScale, xAccessor, yScale, yAccessor, maxYDomainForZeroData); - const { computeNumTicks: computeLeftAxisNumTicks, ...mergedLeftAxisProps } = useMergedProps>( + const { computeNumTicks: computeLeftAxisNumTicks, ...mergedLeftAxisProps } = useMergedProps( leftAxisProps, lineChartDefault.leftAxisProps, ); - const { computeNumTicks: computeBottomAxisNumTicks, ...mergedBottomAxisProps } = useMergedProps< - AxisProps - >(bottomAxisProps, lineChartDefault.bottomAxisProps); + const { computeNumTicks: computeBottomAxisNumTicks, ...mergedBottomAxisProps } = useMergedProps( + bottomAxisProps, + lineChartDefault.bottomAxisProps, + ); - const { computeNumTicks: computeGridNumTicks, ...mergedGridProps } = useMergedProps>( + const { computeNumTicks: computeGridNumTicks, ...mergedGridProps } = useMergedProps( gridProps, lineChartDefault.gridProps, ); + const minDataValue = useMinDataValue(data, yAccessor); + // In case of no data we should render empty graph with axises // but they don't render at all without any data. // To handle this case we will render the same graph with fake data and hide bars @@ -151,10 +97,9 @@ export function LineChart({ {renderGradients?.()} @@ -163,6 +108,11 @@ export function LineChart({ numTicks={computeLeftAxisNumTicks?.(width, height, internalMargin, data)} {...mergedLeftAxisProps} /> + ({ {...mergedBottomAxisProps} /> {/* Left vertical line for y-axis */} - + {showLeftAxisLine && ( + + )} + {/* Bottom horizontal line for x-axis */} - + {showBottomAxisLine && ( + + )} {showGrid && ( ({ /> )} - + dataKey="line-chart-seria-01" data={data} fill={!isEmpty ? areaColor : 'transparent'} curve={curveMonotoneX} lineProps={{ stroke: !isEmpty ? lineColor : 'transparent' }} + // adjust baseline to show area correctly with negative values in data + y0Accessor={() => Math.min(minDataValue, 0)} {...accessors} /> - + {showGlyphOnSingleDataPoint && data.length === 1 && ( + + dataKey="line-chart-seria-01" + data={data} + renderGlyph={renderGlyphOnSingleDataPoint} + {...accessors} + /> + )} + + snapTooltipToDatumX snapTooltipToDatumY showVerticalCrosshair diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx index 904f4dbbc360d..e74e5d9a9e446 100644 --- a/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx +++ b/datahub-web-react/src/alchemy-components/components/LineChart/components.tsx @@ -1,8 +1,8 @@ import { colors } from '@src/alchemy-components/theme'; -import React, { useEffect, useRef } from 'react'; +import React, { forwardRef, useEffect, useRef } from 'react'; import styled from 'styled-components'; import { GLYPH_DROP_SHADOW_FILTER } from './constants'; -import { TooltipGlyphProps } from './types'; +import { GlyphProps } from './types'; export const ChartWrapper = styled.div` width: 100%; @@ -11,7 +11,24 @@ export const ChartWrapper = styled.div` cursor: pointer; `; -export const TooltipGlyph = ({ x, y }: TooltipGlyphProps) => { +export const Glyph = ({ x, y }: GlyphProps): React.ReactElement => { + return ( + + + + + ); +}; + +export const GlyphWithRef = forwardRef((props, ref): React.ReactElement => { + return ( + + + + ); +}); + +export const TooltipGlyph = (props: GlyphProps): React.ReactElement => { const ref = useRef(null); // FYI: Change size of parent SVG to prevent showing window's horizontal scrolling @@ -27,10 +44,5 @@ export const TooltipGlyph = ({ x, y }: TooltipGlyphProps) => { } }, [ref]); - return ( - - - - - ); + return ; }; diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/customTooltip.css b/datahub-web-react/src/alchemy-components/components/LineChart/customTooltip.css new file mode 100644 index 0000000000000..8dd55dd0fab23 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/LineChart/customTooltip.css @@ -0,0 +1,4 @@ +/* Read the comment in ./LineChart.tsx */ +.visx-tooltip { + z-index: 1000; +} diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/defaults.tsx b/datahub-web-react/src/alchemy-components/components/LineChart/defaults.tsx new file mode 100644 index 0000000000000..cb6eb5296738a --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/LineChart/defaults.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { TickLabelProps } from '@visx/axis'; +import { colors } from '@src/alchemy-components/theme'; +import dayjs from 'dayjs'; +import { LinearGradient } from '@visx/gradient'; +import { Datum, LineChartProps } from './types'; +import { abbreviateNumber } from '../dataviz/utils'; +import { roundToEven } from './utils'; +import { GLYPH_DROP_SHADOW_FILTER } from './constants'; +import { Glyph, TooltipGlyph } from './components'; + +const commonTickLabelProps: TickLabelProps = { + fontSize: 10, + fontFamily: 'Mulish', + fill: colors.gray[1700], +}; + +export const lineChartDefault: LineChartProps = { + data: [], + isEmpty: false, + + xScale: { type: 'time' }, + yScale: { type: 'linear', nice: true, round: true, zero: true }, + + lineColor: colors.violet[500], + areaColor: 'url(#line-gradient)', + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + + leftAxisProps: { + tickFormat: abbreviateNumber, + tickLabelProps: { + ...commonTickLabelProps, + textAnchor: 'end', + width: 50, + }, + computeNumTicks: () => 5, + hideAxisLine: true, + hideTicks: true, + }, + showLeftAxisLine: false, + bottomAxisProps: { + tickFormat: (x) => dayjs(x).format('D MMM'), + tickLabelProps: { + ...commonTickLabelProps, + textAnchor: 'middle', + verticalAnchor: 'start', + }, + computeNumTicks: (width, _, margin, data) => { + const widthOfTick = 80; + const widthOfAxis = width - margin.right - margin.left; + const maxCountOfTicks = Math.ceil(widthOfAxis / widthOfTick); + const numOfTicks = roundToEven(maxCountOfTicks / 2); + return Math.max(Math.min(numOfTicks, data.length - 1), 1); + }, + hideAxisLine: true, + hideTicks: true, + }, + showBottomAxisLine: true, + gridProps: { + rows: true, + columns: false, + stroke: '#e0e0e0', + computeNumTicks: () => 5, + lineStyle: {}, + }, + + renderGradients: () => ( + + ), + toolbarVerticalCrosshairStyle: { + stroke: colors.white, + strokeWidth: 2, + filter: GLYPH_DROP_SHADOW_FILTER, + }, + renderTooltipGlyph: (props) => , + showGlyphOnSingleDataPoint: true, + renderGlyphOnSingleDataPoint: Glyph, +}; diff --git a/datahub-web-react/src/alchemy-components/components/LineChart/types.ts b/datahub-web-react/src/alchemy-components/components/LineChart/types.ts index 818a353720e6a..2e224715e268f 100644 --- a/datahub-web-react/src/alchemy-components/components/LineChart/types.ts +++ b/datahub-web-react/src/alchemy-components/components/LineChart/types.ts @@ -1,34 +1,39 @@ import { AxisScaleOutput } from '@visx/axis'; import { ScaleConfig } from '@visx/scale'; -import { Margin } from '@visx/xychart'; -import { RenderTooltipGlyphProps } from '@visx/xychart/lib/components/Tooltip'; +import { GlyphProps as VisxGlyphProps, Margin } from '@visx/xychart'; import React from 'react'; -import { AxisProps, GridProps } from '../BarChart/types'; +import { AxisProps, BaseDatum, GridProps } from '../BarChart/types'; -export type LineChartProps = { - data: DatumType[]; +export type Datum = BaseDatum; + +export type LineChartProps = { + data: Datum[]; isEmpty?: boolean; - xAccessor: (datum: DatumType) => string | number; - yAccessor: (datum: DatumType) => number; xScale?: ScaleConfig; yScale?: ScaleConfig; maxYDomainForZeroData?: number; lineColor?: string; areaColor?: string; - margin?: Margin; + margin?: Partial; - leftAxisProps?: AxisProps; - bottomAxisProps?: AxisProps; - gridProps?: GridProps; + leftAxisProps?: AxisProps; + showLeftAxisLine?: boolean; + bottomAxisProps?: AxisProps; + showBottomAxisLine?: boolean; + gridProps?: GridProps; - popoverRenderer?: (datum: DatumType) => React.ReactNode; + popoverRenderer?: (datum: Datum) => React.ReactNode; renderGradients?: () => React.ReactNode; toolbarVerticalCrosshairStyle?: React.SVGProps; - renderTooltipGlyph?: (props: RenderTooltipGlyphProps) => React.ReactNode | undefined; + renderTooltipGlyph?: (props: GlyphProps) => React.ReactElement | null; + showGlyphOnSingleDataPoint?: boolean; + renderGlyphOnSingleDataPoint?: React.FC; }; +export type GlyphProps = VisxGlyphProps; + export type TooltipGlyphProps = { x: number; y: number; diff --git a/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx b/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx index a65d9e355363f..825566a4461ca 100644 --- a/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx +++ b/datahub-web-react/src/alchemy-components/components/Pills/Pill.tsx @@ -11,6 +11,7 @@ export const SUPPORTED_CONFIGURATIONS: Record { if (initialValues && shouldAlwaysSyncParentValues) { - const filteredOptions = selectedOptions.filter((option) => - initialValues.some((initial) => initial.value === option.value), - ); - if (filteredOptions.length !== selectedOptions.length) { - setSelectedOptions(filteredOptions); + // Check if selectedOptions and initialValues are different + const areDifferent = JSON.stringify(selectedOptions) !== JSON.stringify(initialValues); + + if (initialValues && areDifferent) { + setSelectedOptions(initialValues); } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/datahub-web-react/src/alchemy-components/components/SelectItemsPopover/SelectItems.tsx b/datahub-web-react/src/alchemy-components/components/SelectItemsPopover/SelectItems.tsx index 75b14b365b325..feac166e7f0da 100644 --- a/datahub-web-react/src/alchemy-components/components/SelectItemsPopover/SelectItems.tsx +++ b/datahub-web-react/src/alchemy-components/components/SelectItemsPopover/SelectItems.tsx @@ -5,7 +5,7 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Entity, EntityType } from '@src/types.generated'; import { CheckboxValueType } from 'antd/lib/checkbox/Group'; import { REDESIGN_COLORS } from '@src/app/entityV2/shared/constants'; -import { AcrylListSearch } from '@src/app/entityV2/shared/components/ListSearch/AcrylListSearch'; +import { InlineListSearch } from '@src/app/entityV2/shared/components/search/InlineListSearch'; import { useEntityRegistry } from '@src/app/useEntityRegistry'; import { useEntityOperations } from './hooks'; // Import your custom hook import { SelectItemCheckboxGroup } from './SelectItemCheckboxGroup'; @@ -148,7 +148,7 @@ export const SelectItems: React.FC = ({ const emptyMessage = `No ${entityName} found`; return ( - ({ {sortedData.map((row: any, index) => { const isExpanded = expandable?.expandedRowKeys?.includes(row?.name); // Check if row is expanded const canExpand = expandable?.rowExpandable?.(row); // Check if row is expandable - + const rowKey = `row-${index}-${sortColumn ?? 'none'}-${sortOrder ?? 'none'}`; return ( <> {/* Render the main row */} { if (canExpand) onExpand?.(row); // Handle row expansion diff --git a/datahub-web-react/src/alchemy-components/components/Text/Text.tsx b/datahub-web-react/src/alchemy-components/components/Text/Text.tsx index 89122afbfcc8b..6f0bcba349a60 100644 --- a/datahub-web-react/src/alchemy-components/components/Text/Text.tsx +++ b/datahub-web-react/src/alchemy-components/components/Text/Text.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TextProps } from './types'; -import { P, Div, Span } from './components'; +import { P, Div, Span, Pre } from './components'; export const textDefaults: TextProps = { type: 'p', @@ -27,6 +27,8 @@ export const Text = ({ return
{children}
; case 'span': return {children}; + case 'pre': + return
{children}
; default: return

{children}

; } diff --git a/datahub-web-react/src/alchemy-components/components/Text/components.ts b/datahub-web-react/src/alchemy-components/components/Text/components.ts index f2cd045a0e6f7..0641ec34388bd 100644 --- a/datahub-web-react/src/alchemy-components/components/Text/components.ts +++ b/datahub-web-react/src/alchemy-components/components/Text/components.ts @@ -48,3 +48,7 @@ export const Span = styled.span({ ...baseStyles, ...textStyles }, (props: TextPr export const Div = styled.div({ ...baseStyles, ...textStyles }, (props: TextProps) => ({ ...propStyles(props as TextProps, true), })); + +export const Pre = styled.pre({ ...baseStyles, ...textStyles }, (props: TextProps) => ({ + ...propStyles(props as TextProps, true), +})); diff --git a/datahub-web-react/src/alchemy-components/components/Text/types.ts b/datahub-web-react/src/alchemy-components/components/Text/types.ts index 96b3bfe2ba230..c56eb58a93a7e 100644 --- a/datahub-web-react/src/alchemy-components/components/Text/types.ts +++ b/datahub-web-react/src/alchemy-components/components/Text/types.ts @@ -2,7 +2,7 @@ import { HTMLAttributes } from 'react'; import { Color, FontSizeOptions, FontColorOptions, FontWeightOptions, SpacingOptions } from '@components/theme/config'; export interface TextProps extends HTMLAttributes { - type?: 'span' | 'p' | 'div'; + type?: 'span' | 'p' | 'div' | 'pre'; size?: FontSizeOptions; color?: FontColorOptions; colorLevel?: keyof Color; diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.stories.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.stories.tsx new file mode 100644 index 0000000000000..ed2ec4fd6c527 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.stories.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { BADGE } from '@geometricpanda/storybook-addon-badges'; + +import WhiskerChart from './WhiskerChart'; +import { DEFAULT_BOX_SIZE, DEFAULT_GAP_BETWEEN_WHISKERS } from './constants'; + +// Auto Docs +const meta = { + title: 'Charts / WhiskerChart', + component: WhiskerChart, + + // Display Properties + parameters: { + layout: 'centered', + badges: [BADGE.STABLE, 'readyForDesignReview'], + docs: { + subtitle: 'Used to render text and paragraphs within an interface.', + }, + }, + + // Component-level argTypes + argTypes: { + data: { + description: + 'An array of WhiskerDatum objects representing statistical data and optional color scheme for rendering whisker plots', + table: { + type: { + summary: 'WhiskerDatum[]', + }, + }, + }, + boxSize: { + description: 'Size of box', + table: { + defaultValue: { summary: `${DEFAULT_BOX_SIZE}` }, + }, + control: { + type: 'number', + }, + }, + gap: { + description: 'Gap between whiskers', + table: { + defaultValue: { summary: `${DEFAULT_GAP_BETWEEN_WHISKERS}` }, + }, + control: { + type: 'number', + }, + }, + axisLabel: { + description: 'Optional label of the axis', + table: { + defaultValue: { summary: 'undefined' }, + type: { summary: 'string' }, + }, + control: { + type: 'text', + }, + }, + renderTooltip: { + description: 'Rendering function to customize tolltip', + }, + renderWhisker: { + description: 'Rendering function to customize whisker', + }, + }, + + // Define default args + args: { + data: [ + { + key: 'test', + min: 5, + firstQuartile: 7, + median: 18, + thirdQuartile: 30, + max: 50, + }, + { + key: 'test2', + min: -10, + firstQuartile: 12, + median: 32, + thirdQuartile: 45, + max: 55, + colorShemeSettings: { + box: 'red', + boxAlternative: 'blue', + medianLine: 'green', + alternative: 'black', + }, + }, + ], + }, +} satisfies Meta; + +export default meta; + +// Stories + +type Story = StoryObj; + +// Basic story is what is displayed 1st in storybook +// Pass props to this so that it can be customized via the UI props panel +export const sandbox: Story = { + tags: ['dev'], + render: (props) => ( +
+ +
+ ), +}; diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.tsx new file mode 100644 index 0000000000000..51982c39e43a1 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/WhiskerChart.tsx @@ -0,0 +1,142 @@ +import { colors } from '@src/alchemy-components/theme'; +import { Axis } from '@visx/axis'; +import { GridColumns } from '@visx/grid'; +import { ParentSize } from '@visx/responsive'; +import { scaleLinear } from '@visx/scale'; +import { BoxPlot } from '@visx/stats'; +import { useTooltip } from '@visx/tooltip'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { + AXIS_LABEL_MARGIN_OFFSET, + AXIS_LABEL_PROPS, + DEFAULT_BOX_SIZE, + DEFAULT_GAP_BETWEEN_WHISKERS, +} from './constants'; +import { whiskerChartDefaults } from './defaults'; +import { InternalWhiskerChartProps, WhiskerChartProps, WhiskerTooltipDatum } from './types'; +import { computeWhiskerOffset } from './utils'; + +const ChartWrapper = styled.div` + width: 100%; + height: 100%; + position: relative; +`; + +function InternalWhiskerChart({ + data, + width, + height, + tooltip, + boxSize = whiskerChartDefaults.boxSize, + gap = whiskerChartDefaults.gap, + axisLabel, + renderTooltip = whiskerChartDefaults.renderTooltip, + renderWhisker = whiskerChartDefaults.renderWhisker, +}: InternalWhiskerChartProps) { + const axisLabelMarginOffset = axisLabel !== undefined ? AXIS_LABEL_MARGIN_OFFSET : 0; + const margin = { left: 10, top: 0, right: 10, bottom: 20 + axisLabelMarginOffset }; + + const finalBoxSize = boxSize ?? DEFAULT_BOX_SIZE; + const finalGap = gap ?? DEFAULT_GAP_BETWEEN_WHISKERS; + + const minY = 0; + const maxY = height - margin.bottom; + const minX = margin.left; + const maxX = width - margin.right; + const chartHeight = maxY - minY; + const chartWidth = maxX - minX; + + const dataWithOffsets = useMemo(() => { + return data.map((datum, index) => ({ + datum, + offset: computeWhiskerOffset(data.length, index, finalBoxSize, chartHeight, finalGap), + })); + }, [data, chartHeight, finalBoxSize, finalGap]); + + const minValue = useMemo(() => Math.min(...data.map((datum) => datum.min)), [data]); + const maxValue = useMemo(() => Math.max(...data.map((datum) => datum.max)), [data]); + + const xScale = useMemo(() => { + // 5% paddings to left and right sides + const valuePadding = (maxValue - minValue) * 0.05; + return scaleLinear({ + range: [minX, maxX], + round: true, + domain: [minValue - valuePadding, maxValue + valuePadding], + nice: true, + }); + }, [minX, maxX, minValue, maxValue]); + + return ( + + + + {dataWithOffsets.map(({ datum, offset }) => ( + + {renderWhisker ? (props) => renderWhisker({ datum, tooltip, ...props }) : undefined} + + ))} + + + + + {tooltip.tooltipOpen && + renderTooltip?.({ + x: tooltip.tooltipLeft, + y: tooltip.tooltipTop, + minY, + maxY, + datum: tooltip.tooltipData, + })} + + ); +} + +export default function WhiskerChart(props: WhiskerChartProps) { + const tooltip = useTooltip(); + + return ( + + + {({ width, height }) => ( + + )} + + + ); +} diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/GlyphWithLineAndPopover.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/GlyphWithLineAndPopover.tsx new file mode 100644 index 0000000000000..d2bbfe11bef92 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/GlyphWithLineAndPopover.tsx @@ -0,0 +1,75 @@ +import React, { useCallback } from 'react'; +import { Popover } from '../../Popover'; +import { Text } from '../../Text'; +import { + DEFAULT_COLOR_SHEME, + WHISKER_METRIC_ATTRIBUTE_NAMES, + WHISKER_METRIC_NAMES as WHISKER_METRIC_LABELS, +} from '../constants'; +import { TooltipRendererProps } from '../types'; + +const RADIUS = 6; + +const SHADOW = ` + drop-shadow(0px 1px 3px rgba(33, 23, 95, 0.30)) + drop-shadow(0px 2px 5px rgba(33, 23, 95, 0.25)) + drop-shadow(0px -2px 5px rgba(33, 23, 95, 0.25)) +`; + +export default function GlyphWithLineAndPopover({ x, y, minY, maxY, datum }: TooltipRendererProps) { + const renderPopoverContent = useCallback(() => { + if (!datum) return null; + + const label = WHISKER_METRIC_LABELS[datum.type]; + const value = datum[WHISKER_METRIC_ATTRIBUTE_NAMES[datum.type]]; + + return ( + <> + + {label}:  + + + {value} + + + ); + }, [datum]); + + if (y === undefined || x === undefined) return null; + + const color = datum?.colorShemeSettings?.alternative ?? DEFAULT_COLOR_SHEME.alternative; + + return ( + + + + renderPopoverContent()} placement="topLeft" align={{ offset: [15, 15] }} open> + + + + ); +} diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/MetricPoint.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/MetricPoint.tsx new file mode 100644 index 0000000000000..9272db931b1a5 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/MetricPoint.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styled from 'styled-components'; +import { MetricPointProps } from '../types'; + +const StyledRect = styled.rect` + cursor: pointer; +`; + +const RECT_WIDTH = 16; + +export default function MetricPoint({ + pointX, + topOfWhiskerBar, + heightOfWhiskerBar, + overHandler, + leaveHandler, +}: MetricPointProps) { + return ( + + ); +} diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/WhiskerRenderer.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/WhiskerRenderer.tsx new file mode 100644 index 0000000000000..56440ef7260f1 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/components/WhiskerRenderer.tsx @@ -0,0 +1,136 @@ +import { LinearGradient } from '@visx/gradient'; +import { BarRounded } from '@visx/shape'; +import React, { useState } from 'react'; +import { DEFAULT_COLOR_SHEME } from '../constants'; +import { WhiskerMetricType, WhiskerRenderProps } from '../types'; +import MetricPoint from './MetricPoint'; + +export default function WhiskerRenderer({ + datum, + tooltip, + box, + min, + minToFirst, + median, + maxToThird, + max, +}: WhiskerRenderProps) { + const [isMedianLineVisible, setIsMedianLineVisible] = useState(true); + + const maxY = box.y1; + const height = box.y2; + const centerY = minToFirst.y1; + + // FYI: splitting the bar into two halves along the median to display different gradients on them + const leftBarX = box.x1; + const leftBarWidth = median.x1 - leftBarX; + const rightBarX = median.x1; + const rightBarWidth = maxToThird.x2 - rightBarX; + + const leftBarGradientId = `left-gradient-${datum.key}`; + const rightBarGradientId = `right-gradient-${datum.key}`; + + const colorSheme = datum?.colorShemeSettings ?? DEFAULT_COLOR_SHEME; + + const handleMetricOver = (pointX: number, metricType: WhiskerMetricType) => { + if (metricType === WhiskerMetricType.Median) setIsMedianLineVisible(false); + + tooltip.showTooltip({ + tooltipData: { ...datum, type: metricType }, + tooltipLeft: pointX, + tooltipTop: centerY, + }); + }; + + const handleMetricLeave = (metricType: WhiskerMetricType) => { + if (metricType === WhiskerMetricType.Median) setIsMedianLineVisible(true); + tooltip.hideTooltip(); + }; + + return ( + <> + + + + {/* Min to first quartile whisker */} + + + handleMetricOver(min.x1, WhiskerMetricType.Min)} + leaveHandler={() => handleMetricLeave(WhiskerMetricType.Min)} + /> + + {/* Left half of bar */} + + handleMetricOver(leftBarX, WhiskerMetricType.FirstQuartile)} + leaveHandler={() => handleMetricLeave(WhiskerMetricType.FirstQuartile)} + /> + + {/* Right half of bar */} + + handleMetricOver(rightBarX + rightBarWidth, WhiskerMetricType.ThirdQuartile)} + leaveHandler={() => handleMetricLeave(WhiskerMetricType.ThirdQuartile)} + /> + + {/* Median */} + {isMedianLineVisible && ( + + )} + handleMetricOver(median.x1, WhiskerMetricType.Median)} + leaveHandler={() => handleMetricLeave(WhiskerMetricType.Median)} + /> + + {/* Third quartile to max whisker */} + + + handleMetricOver(max.x1, WhiskerMetricType.Max)} + leaveHandler={() => handleMetricLeave(WhiskerMetricType.Max)} + /> + + ); +} diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/constants.ts b/datahub-web-react/src/alchemy-components/components/WhiskerChart/constants.ts new file mode 100644 index 0000000000000..13d6ced89cc91 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/constants.ts @@ -0,0 +1,37 @@ +import { TextProps } from '@visx/text/lib/Text'; +import { ColorSchemeSettings, WhiskerMetricType } from './types'; + +export const DEFAULT_COLOR_SHEME: ColorSchemeSettings = { + box: '#705EE4', + boxAlternative: '#CAC3F1', + medianLine: '#2200F9', + alternative: '#533FD1', +}; + +export const WHISKER_METRIC_NAMES = { + [WhiskerMetricType.Max]: 'Max', + [WhiskerMetricType.FirstQuartile]: 'First Quartile', + [WhiskerMetricType.Median]: 'Median', + [WhiskerMetricType.ThirdQuartile]: 'Third Quartile', + [WhiskerMetricType.Min]: 'Min', +}; + +export const WHISKER_METRIC_ATTRIBUTE_NAMES = { + [WhiskerMetricType.Max]: 'max', + [WhiskerMetricType.FirstQuartile]: 'firstQuartile', + [WhiskerMetricType.Median]: 'median', + [WhiskerMetricType.ThirdQuartile]: 'thirdQuartile', + [WhiskerMetricType.Min]: 'min', +}; + +export const DEFAULT_BOX_SIZE = 34; +export const DEFAULT_GAP_BETWEEN_WHISKERS = 10; + +export const AXIS_LABEL_PROPS: Partial = { + fontSize: 12, + fontFamily: 'Mulish', + fontWeight: 600, + textAnchor: 'middle', +}; + +export const AXIS_LABEL_MARGIN_OFFSET = 30; diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/defaults.tsx b/datahub-web-react/src/alchemy-components/components/WhiskerChart/defaults.tsx new file mode 100644 index 0000000000000..678ced5fa9dee --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/defaults.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import GlyphWithLineAndPopover from './components/GlyphWithLineAndPopover'; +import WhiskerRenderer from './components/WhiskerRenderer'; +import { DEFAULT_BOX_SIZE, DEFAULT_GAP_BETWEEN_WHISKERS } from './constants'; +import { WhiskerChartProps } from './types'; + +export const whiskerChartDefaults: Omit = { + boxSize: DEFAULT_BOX_SIZE, + gap: DEFAULT_GAP_BETWEEN_WHISKERS, + renderTooltip: (props) => , + renderWhisker: (props) => , +}; diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/index.ts b/datahub-web-react/src/alchemy-components/components/WhiskerChart/index.ts new file mode 100644 index 0000000000000..51fe2449bee6a --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/index.ts @@ -0,0 +1 @@ +export { default as WhiskerChart } from './WhiskerChart'; diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/types.ts b/datahub-web-react/src/alchemy-components/components/WhiskerChart/types.ts new file mode 100644 index 0000000000000..84f4d723859b5 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/types.ts @@ -0,0 +1,68 @@ +import { ChildRenderProps } from '@visx/stats/lib/types'; +import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip'; +import React from 'react'; + +export interface TooltipRendererProps { + x?: number | undefined; + y?: number | undefined; + minY?: number; + maxY?: number; + datum?: WhiskerTooltipDatum; +} + +export interface ColorSchemeSettings { + box: string; + boxAlternative: string; + medianLine: string; + alternative: string; +} + +export interface WhiskerDatum { + key: string; + min: number; + firstQuartile: number; + median: number; + thirdQuartile: number; + max: number; + colorShemeSettings?: ColorSchemeSettings; +} + +export enum WhiskerMetricType { + Min = 'MIN', + FirstQuartile = 'FIRST_QUARTILE', + Median = 'MEDIAN', + ThirdQuartile = 'THIRD_QUARTILE', + Max = 'MAX', +} + +export interface WhiskerTooltipDatum extends WhiskerDatum { + type: WhiskerMetricType; +} + +export interface WhiskerChartProps { + data: WhiskerDatum[]; + boxSize?: number; + gap?: number; + axisLabel?: string; + renderTooltip?: (props: TooltipRendererProps) => React.ReactNode; + renderWhisker?: (props: WhiskerRenderProps) => React.ReactNode; +} + +export type InternalWhiskerChartProps = WhiskerChartProps & { + width: number; + height: number; + tooltip: UseTooltipParams; +}; + +export type WhiskerRenderProps = ChildRenderProps & { + datum: WhiskerDatum; + tooltip: UseTooltipParams; +}; + +export interface MetricPointProps { + pointX: number; + topOfWhiskerBar: number; + heightOfWhiskerBar: number; + overHandler: () => void; + leaveHandler: () => void; +} diff --git a/datahub-web-react/src/alchemy-components/components/WhiskerChart/utils.ts b/datahub-web-react/src/alchemy-components/components/WhiskerChart/utils.ts new file mode 100644 index 0000000000000..5fabf8dfab725 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/WhiskerChart/utils.ts @@ -0,0 +1,21 @@ +/** + * Compute top offset to render a few "whiskers" on on chart + */ +export function computeWhiskerOffset( + numberOfAllWhiskers: number, + numberOfCurrentWhisker: number, + whiskerBoxSize: number, + allWidth: number, + gapBetweenWhiskers: number, +): number { + const numRectangles = numberOfAllWhiskers; + + // Calculate the total height occupied by rectangles and gaps + const totalWidthRequired = numRectangles * whiskerBoxSize + (numRectangles - 1) * gapBetweenWhiskers; + + // Calculate the starting Y position to center the group of rectangles vertically + const start = (allWidth - totalWidthRequired) / 2; + + const offset = start + numberOfCurrentWhisker * (whiskerBoxSize + gapBetweenWhiskers); + return offset; +} diff --git a/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts b/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts index 15e2783d2905d..61e638ebc7747 100644 --- a/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts +++ b/datahub-web-react/src/alchemy-components/components/dataviz/utils.ts @@ -2,10 +2,12 @@ export const abbreviateNumber = (str) => { const number = parseFloat(str); if (Number.isNaN(number)) return str; - if (number < 1000) return number; + const sign = number < 0 ? '-' : ''; + const absoluteNumber = Math.abs(number); + if (absoluteNumber < 1000) return number; const abbreviations = ['K', 'M', 'B', 'T']; - const index = Math.floor(Math.log10(number) / 3); + const index = Math.floor(Math.log10(absoluteNumber) / 3); const suffix = abbreviations[index - 1]; - const shortNumber = number / 10 ** (index * 3); - return `${shortNumber}${suffix}`; + const shortNumber = absoluteNumber / 10 ** (index * 3); + return `${sign}${shortNumber}${suffix}`; }; diff --git a/datahub-web-react/src/alchemy-components/index.ts b/datahub-web-react/src/alchemy-components/index.ts index 62c444985e3fc..bab7282b645de 100644 --- a/datahub-web-react/src/alchemy-components/index.ts +++ b/datahub-web-react/src/alchemy-components/index.ts @@ -29,3 +29,4 @@ export * from './components/Text'; export * from './components/TextArea'; export * from './components/Timeline'; export * from './components/Tooltip'; +export * from './components/WhiskerChart'; diff --git a/datahub-web-react/src/app/DataHubTitle.tsx b/datahub-web-react/src/app/DataHubTitle.tsx new file mode 100644 index 0000000000000..aace36bd38cba --- /dev/null +++ b/datahub-web-react/src/app/DataHubTitle.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Helmet } from 'react-helmet-async'; +import { useLocation } from 'react-router'; +import { useCustomTheme } from '../customThemeContext'; +import { useAppConfig } from './useAppConfig'; + +const PATH_FRAGMENT_TO_TITLE_OVERRIDES = { + sso: 'SSO', + oidc: 'OIDC', +}; + +export default function DataHubTitle() { + const location = useLocation(); + const { config } = useAppConfig(); + const { theme } = useCustomTheme(); + + const title = + location.pathname + .split('/') + .filter((word) => word !== '') + .map((rawWord) => { + if (rawWord in PATH_FRAGMENT_TO_TITLE_OVERRIDES) { + return PATH_FRAGMENT_TO_TITLE_OVERRIDES[rawWord]; + } + // ie. personal-notifications -> Personal Notifications + const words = rawWord.split('-'); + return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + }) + .join(' | ') || + config?.visualConfig?.appTitle || + theme?.content?.title; + + if (!title) return null; + return ( + + {title} + + ); +} diff --git a/datahub-web-react/src/app/ProtectedRoutes.tsx b/datahub-web-react/src/app/ProtectedRoutes.tsx index 9d9f24e3da793..2cf3c724ed772 100644 --- a/datahub-web-react/src/app/ProtectedRoutes.tsx +++ b/datahub-web-react/src/app/ProtectedRoutes.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { Switch, Route, useLocation, useHistory } from 'react-router-dom'; import { Layout } from 'antd'; import styled from 'styled-components'; +import DataHubTitle from './DataHubTitle'; import { HomePage } from './home/HomePage'; import { HomePage as HomePageV2 } from './homeV2/HomePage'; import { SearchRoutes } from './SearchRoutes'; @@ -43,12 +44,13 @@ export const ProtectedRoutes = (): JSX.Element => { return ( + } /> } /> } /> - } /> + diff --git a/datahub-web-react/src/app/SearchRoutes.tsx b/datahub-web-react/src/app/SearchRoutes.tsx index 9a635bec04637..296c89f3c275e 100644 --- a/datahub-web-react/src/app/SearchRoutes.tsx +++ b/datahub-web-react/src/app/SearchRoutes.tsx @@ -6,14 +6,14 @@ import { BrowseResultsPage } from './browse/BrowseResultsPage'; import { BusinessAttributes } from './businessAttribute/BusinessAttributes'; import { useUserContext } from './context/useUserContext'; import DomainRoutes from './domain/DomainRoutes'; +import { ManageDomainsPage } from './domain/ManageDomainsPage'; +import StructuredProperties from './govern/structuredProperties/StructuredProperties'; import { useAppConfig, useBusinessAttributesFlag, useIsAppConfigContextLoaded, useIsNestedDomainsEnabled, } from './useAppConfig'; -import { ManageDomainsPage } from './domain/ManageDomainsPage'; -import StructuredProperties from './govern/structuredProperties/StructuredProperties'; import { EntityPage } from './entity/EntityPage'; import { EntityPage as EntityPageV2 } from './entityV2/EntityPage'; diff --git a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx index 51b66c8c2a41d..faad2600dcd61 100644 --- a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx @@ -19,6 +19,7 @@ import DataProductSection from '../shared/containers/profile/sidebar/DataProduct import { getDataProduct } from '../shared/utils'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; +import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; /** * Definition of the DataHub MLFeature entity. @@ -95,6 +96,14 @@ export class MLFeatureEntity implements Entity { name: 'Properties', component: PropertiesTab, }, + { + name: 'Incidents', + component: IncidentTab, + getDynamicName: (_, mlFeature) => { + const activeIncidentCount = mlFeature?.mlFeature?.activeIncidents?.total; + return `Incidents${(activeIncidentCount && ` (${activeIncidentCount})`) || ''}`; + }, + }, ]} sidebarSections={this.getSidebarSections()} /> diff --git a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx index b5ebbd39621cb..12e78ea76185f 100644 --- a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx @@ -19,6 +19,7 @@ import MlModelFeaturesTab from './profile/MlModelFeaturesTab'; import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; +import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; /** * Definition of the DataHub MlModel entity. @@ -126,6 +127,14 @@ export class MLModelEntity implements Entity { name: 'Properties', component: PropertiesTab, }, + { + name: 'Incidents', + component: IncidentTab, + getDynamicName: (_, mlModel) => { + const activeIncidentCount = mlModel?.mlModel?.activeIncidents?.total; + return `Incidents${(activeIncidentCount && ` (${activeIncidentCount})`) || ''}`; + }, + }, ]} sidebarSections={this.getSidebarSections()} /> diff --git a/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/RichTextInput.tsx b/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/RichTextInput.tsx index acce9eb889627..5456d714c1491 100644 --- a/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/RichTextInput.tsx +++ b/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/RichTextInput.tsx @@ -10,6 +10,8 @@ const StyledEditor = styled(Editor)` width: 75%; min-width: 585px; max-width: 700px; + max-height: 300px; + overflow: auto; &&& { .remirror-editor { diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx index 29073d6164f82..797cd9d09f140 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx @@ -30,7 +30,7 @@ export const SidebarSiblingsSection = () => {
- +
); @@ -58,11 +58,10 @@ export const SidebarSiblingsSection = () => { return (
- +
diff --git a/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/useUrnInput.tsx b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/useUrnInput.tsx index 4f621f7018f12..fba2ef933c933 100644 --- a/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/useUrnInput.tsx +++ b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/useUrnInput.tsx @@ -1,13 +1,14 @@ import { Tag } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { isEqual } from 'lodash'; import { Entity, PropertyCardinality, StructuredPropertyEntity } from '../../../../../../../types.generated'; import { useGetSearchResultsForMultipleLazyQuery } from '../../../../../../../graphql/search.generated'; +import usePrevious from '../../../../../../shared/usePrevious'; +import { useEntityRegistry } from '../../../../../../useEntityRegistry'; import { useEntityData } from '../../../../EntityContext'; import { getInitialEntitiesForUrnPrompt } from '../utils'; import SelectedEntity from './SelectedEntity'; -import { useEntityRegistry } from '../../../../../../useEntityRegistry'; -import usePrevious from '../../../../../../shared/usePrevious'; const StyleTag = styled(Tag)` margin: 2px; @@ -52,11 +53,13 @@ export default function useUrnInput({ structuredProperty, selectedValues, update const isMultiple = structuredProperty.definition.cardinality === PropertyCardinality.Multiple; const previousEntityUrn = usePrevious(entityData?.urn); + const previousInitial = usePrevious(initialEntities); + useEffect(() => { - if (entityData?.urn !== previousEntityUrn) { + if (entityData?.urn !== previousEntityUrn || !isEqual(previousInitial, initialEntities)) { setSelectedEntities(initialEntities || []); } - }, [entityData?.urn, previousEntityUrn, initialEntities]); + }, [entityData?.urn, previousEntityUrn, initialEntities, previousInitial]); function handleSearch(query: string) { if (query.length > 0) { diff --git a/datahub-web-react/src/app/entityV2/chart/ChartEntity.tsx b/datahub-web-react/src/app/entityV2/chart/ChartEntity.tsx index ba9bbf6b20f4e..41cc7ad0a4336 100644 --- a/datahub-web-react/src/app/entityV2/chart/ChartEntity.tsx +++ b/datahub-web-react/src/app/entityV2/chart/ChartEntity.tsx @@ -232,10 +232,10 @@ export class ChartEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/container/ContainerEntity.tsx b/datahub-web-react/src/app/entityV2/container/ContainerEntity.tsx index a23a1c2e017f6..23e3baea5b790 100644 --- a/datahub-web-react/src/app/entityV2/container/ContainerEntity.tsx +++ b/datahub-web-react/src/app/entityV2/container/ContainerEntity.tsx @@ -149,10 +149,10 @@ export class ContainerEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, // TODO: Add back once entity-level recommendations are complete. // { diff --git a/datahub-web-react/src/app/entityV2/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entityV2/dashboard/DashboardEntity.tsx index 88494f8f2b75b..560fef45a9f59 100644 --- a/datahub-web-react/src/app/entityV2/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dashboard/DashboardEntity.tsx @@ -230,10 +230,10 @@ export class DashboardEntity implements Entity { component: SidebarTagsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/dataFlow/DataFlowEntity.tsx b/datahub-web-react/src/app/entityV2/dataFlow/DataFlowEntity.tsx index 2f6f217d7e648..0284503ca96f8 100644 --- a/datahub-web-react/src/app/entityV2/dataFlow/DataFlowEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dataFlow/DataFlowEntity.tsx @@ -154,10 +154,10 @@ export class DataFlowEntity implements Entity { component: SidebarTagsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/dataJob/DataJobEntity.tsx b/datahub-web-react/src/app/entityV2/dataJob/DataJobEntity.tsx index 7fd7d21314ec8..a3a179457e834 100644 --- a/datahub-web-react/src/app/entityV2/dataJob/DataJobEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dataJob/DataJobEntity.tsx @@ -28,6 +28,7 @@ import SidebarEntityHeader from '../shared/containers/profile/sidebar/SidebarEnt import { SidebarGlossaryTermsSection } from '../shared/containers/profile/sidebar/SidebarGlossaryTermsSection'; import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection'; import StatusSection from '../shared/containers/profile/sidebar/shared/StatusSection'; +import { SidebarDataJobTransformationLogicSection } from '../shared/containers/profile/sidebar/SidebarLogicSection'; import { getDataForEntityType } from '../shared/containers/profile/utils'; import SidebarStructuredProperties from '../shared/sidebarSection/SidebarStructuredProperties'; import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'; @@ -161,15 +162,16 @@ export class DataJobEntity implements Entity { { component: SidebarAboutSection }, { component: SidebarNotesSection }, { component: SidebarLineageSection }, + { component: SidebarDataJobTransformationLogicSection }, { component: SidebarOwnerSection }, { component: SidebarDomainSection }, { component: DataProductSection }, { component: SidebarGlossaryTermsSection }, { component: SidebarTagsSection }, - { component: StatusSection }, { component: SidebarStructuredProperties, }, + { component: StatusSection }, ]; getSidebarTabs = () => [ diff --git a/datahub-web-react/src/app/entityV2/dataProcessInstance/DataProcessInstanceEntity.tsx b/datahub-web-react/src/app/entityV2/dataProcessInstance/DataProcessInstanceEntity.tsx index 43aa426741adc..97b84b8122286 100644 --- a/datahub-web-react/src/app/entityV2/dataProcessInstance/DataProcessInstanceEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dataProcessInstance/DataProcessInstanceEntity.tsx @@ -1,7 +1,7 @@ import DataProcessInstanceSummary from '@src/app/entity/dataProcessInstance/profile/DataProcessInstanceSummary'; +import { globalEntityRegistryV2 } from '@app/EntityRegistryProvider'; import { GenericEntityProperties } from '@app/entity/shared/types'; import { Entity as GraphQLEntity } from '@types'; -import { globalEntityRegistryV2 } from '@app/EntityRegistryProvider'; import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '@app/entityV2/Entity'; import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile'; import SidebarEntityHeader from '@app/entityV2/shared/containers/profile/sidebar/SidebarEntityHeader'; @@ -79,7 +79,9 @@ export class DataProcessInstanceEntity implements Entity { useEntityQuery={this.useEntityQuery} // useUpdateQuery={useUpdateDataProcessInstanceMutation} getOverrideProperties={this.getOverridePropertiesFromEntity} - headerDropdownItems={new Set([EntityMenuItems.SHARE])} + headerDropdownItems={ + new Set([EntityMenuItems.UPDATE_DEPRECATION, EntityMenuItems.RAISE_INCIDENT, EntityMenuItems.SHARE]) + } tabs={[ { name: 'Summary', diff --git a/datahub-web-react/src/app/entityV2/dataProduct/DataProductEntity.tsx b/datahub-web-react/src/app/entityV2/dataProduct/DataProductEntity.tsx index a8d904e476dda..f5ba47ddf8564 100644 --- a/datahub-web-react/src/app/entityV2/dataProduct/DataProductEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dataProduct/DataProductEntity.tsx @@ -166,10 +166,10 @@ export class DataProductEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entityV2/dataset/DatasetEntity.tsx index 49adc85be0239..476afff4018e8 100644 --- a/datahub-web-react/src/app/entityV2/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entityV2/dataset/DatasetEntity.tsx @@ -288,8 +288,8 @@ export class DatasetEntity implements Entity { }, { component: SidebarDatasetViewDefinitionSection }, { component: SidebarQueryOperationsSection }, - { component: StatusSection }, { component: SidebarStructuredProperties }, + { component: StatusSection }, // { // component: SidebarRecommendationsSection, // }, diff --git a/datahub-web-react/src/app/entityV2/domain/DomainEntity.tsx b/datahub-web-react/src/app/entityV2/domain/DomainEntity.tsx index f5dad4847cb19..54ba2c5dfd896 100644 --- a/datahub-web-react/src/app/entityV2/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entityV2/domain/DomainEntity.tsx @@ -159,10 +159,10 @@ export class DomainEntity implements Entity { component: SidebarOwnerSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/glossaryNode/GlossaryNodeEntity.tsx b/datahub-web-react/src/app/entityV2/glossaryNode/GlossaryNodeEntity.tsx index e819f00f20640..a4d7b415b026d 100644 --- a/datahub-web-react/src/app/entityV2/glossaryNode/GlossaryNodeEntity.tsx +++ b/datahub-web-react/src/app/entityV2/glossaryNode/GlossaryNodeEntity.tsx @@ -130,10 +130,10 @@ class GlossaryNodeEntity implements Entity { component: SidebarOwnerSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/glossaryTerm/GlossaryTermEntity.tsx b/datahub-web-react/src/app/entityV2/glossaryTerm/GlossaryTermEntity.tsx index bb8bad0eeff54..c73ef1790efe7 100644 --- a/datahub-web-react/src/app/entityV2/glossaryTerm/GlossaryTermEntity.tsx +++ b/datahub-web-react/src/app/entityV2/glossaryTerm/GlossaryTermEntity.tsx @@ -171,10 +171,10 @@ export class GlossaryTermEntity implements Entity { }, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/mlFeature/MLFeatureEntity.tsx b/datahub-web-react/src/app/entityV2/mlFeature/MLFeatureEntity.tsx index 3ff8b2b4c01bc..2f5befb2a4c90 100644 --- a/datahub-web-react/src/app/entityV2/mlFeature/MLFeatureEntity.tsx +++ b/datahub-web-react/src/app/entityV2/mlFeature/MLFeatureEntity.tsx @@ -1,4 +1,6 @@ -import { DotChartOutlined, PartitionOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { DotChartOutlined, PartitionOutlined, UnorderedListOutlined, WarningOutlined } from '@ant-design/icons'; +import { IncidentTab } from '@app/entity/shared/tabs/Incident/IncidentTab'; +import TabNameWithCount from '@app/entityV2/shared/tabs/Entity/TabNameWithCount'; import * as React from 'react'; import { useGetMlFeatureQuery } from '../../../graphql/mlFeature.generated'; import { EntityType, MlFeature, SearchResult } from '../../../types.generated'; @@ -109,6 +111,15 @@ export class MLFeatureEntity implements Entity { name: 'Properties', component: PropertiesTab, }, + { + name: 'Incidents', + icon: WarningOutlined, + component: IncidentTab, + getDynamicName: (_, mlFeature, loading) => { + const activeIncidentCount = mlFeature?.mlFeature?.activeIncidents?.total; + return ; + }, + }, ]} sidebarSections={this.getSidebarSections()} sidebarTabs={this.getSidebarTabs()} @@ -141,10 +152,10 @@ export class MLFeatureEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/mlFeatureTable/MLFeatureTableEntity.tsx b/datahub-web-react/src/app/entityV2/mlFeatureTable/MLFeatureTableEntity.tsx index e304b221b5d49..516a182f62b4b 100644 --- a/datahub-web-react/src/app/entityV2/mlFeatureTable/MLFeatureTableEntity.tsx +++ b/datahub-web-react/src/app/entityV2/mlFeatureTable/MLFeatureTableEntity.tsx @@ -135,10 +135,10 @@ export class MLFeatureTableEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/mlModel/MLModelEntity.tsx b/datahub-web-react/src/app/entityV2/mlModel/MLModelEntity.tsx index 3d4547ce4d1d9..518ba9362f76b 100644 --- a/datahub-web-react/src/app/entityV2/mlModel/MLModelEntity.tsx +++ b/datahub-web-react/src/app/entityV2/mlModel/MLModelEntity.tsx @@ -1,4 +1,6 @@ -import { CodeSandboxOutlined, PartitionOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { CodeSandboxOutlined, PartitionOutlined, UnorderedListOutlined, WarningOutlined } from '@ant-design/icons'; +import TabNameWithCount from '@app/entityV2/shared/tabs/Entity/TabNameWithCount'; +import { IncidentTab } from '@app/entityV2/shared/tabs/Incident/IncidentTab'; import { LineageTab } from '@app/entityV2/shared/tabs/Lineage/LineageTab'; import * as React from 'react'; import { useGetMlModelQuery } from '../../../graphql/mlModel.generated'; @@ -123,6 +125,15 @@ export class MLModelEntity implements Entity { name: 'Features', component: MlModelFeaturesTab, }, + { + name: 'Incidents', + icon: WarningOutlined, + component: IncidentTab, + getDynamicName: (_, mlModel, loading) => { + const activeIncidentCount = mlModel?.mlModel?.activeIncidents?.total; + return ; + }, + }, ]} sidebarSections={this.getSidebarSections()} sidebarTabs={this.getSidebarTabs()} @@ -155,10 +166,10 @@ export class MLModelEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/mlModelGroup/MLModelGroupEntity.tsx b/datahub-web-react/src/app/entityV2/mlModelGroup/MLModelGroupEntity.tsx index 25cc9684d8013..9c1c58f97bc22 100644 --- a/datahub-web-react/src/app/entityV2/mlModelGroup/MLModelGroupEntity.tsx +++ b/datahub-web-react/src/app/entityV2/mlModelGroup/MLModelGroupEntity.tsx @@ -141,10 +141,10 @@ export class MLModelGroupEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/mlPrimaryKey/MLPrimaryKeyEntity.tsx b/datahub-web-react/src/app/entityV2/mlPrimaryKey/MLPrimaryKeyEntity.tsx index cb19739476ec7..e6ae2100a08e0 100644 --- a/datahub-web-react/src/app/entityV2/mlPrimaryKey/MLPrimaryKeyEntity.tsx +++ b/datahub-web-react/src/app/entityV2/mlPrimaryKey/MLPrimaryKeyEntity.tsx @@ -129,10 +129,10 @@ export class MLPrimaryKeyEntity implements Entity { component: SidebarGlossaryTermsSection, }, { - component: StatusSection, + component: SidebarStructuredProperties, }, { - component: SidebarStructuredProperties, + component: StatusSection, }, ]; diff --git a/datahub-web-react/src/app/entityV2/ownership/table/ActionsColumn.tsx b/datahub-web-react/src/app/entityV2/ownership/table/ActionsColumn.tsx index 41e07520a0ece..cf4bf9a0fddf4 100644 --- a/datahub-web-react/src/app/entityV2/ownership/table/ActionsColumn.tsx +++ b/datahub-web-react/src/app/entityV2/ownership/table/ActionsColumn.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Dropdown, MenuProps, Popconfirm, Typography, message, notification } from 'antd'; -import { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; +import { CopyOutlined, DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons'; import styled from 'styled-components/macro'; import { OwnershipTypeEntity } from '../../../../types.generated'; import { useDeleteOwnershipTypeMutation } from '../../../../graphql/ownership.generated'; @@ -48,6 +48,10 @@ export const ActionsColumn = ({ ownershipType, setIsOpen, setOwnershipType, refe setOwnershipType(ownershipType); }; + const onCopy = () => { + navigator.clipboard.writeText(ownershipType.urn); + }; + const [deleteOwnershipTypeMutation] = useDeleteOwnershipTypeMutation(); const onDelete = () => { @@ -106,12 +110,23 @@ export const ActionsColumn = ({ ownershipType, setIsOpen, setOwnershipType, refe ), }, + { + key: 'copy', + icon: ( + + + Copy Urn + + ), + }, ]; const onClick: MenuProps['onClick'] = (e) => { const key = e.key as string; if (key === 'edit') { editOnClick(); + } else if (key === 'copy') { + onCopy(); } }; diff --git a/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityMenuActions.tsx b/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityMenuActions.tsx index 96d2e22cb8ee3..0a3d8a4ff4e95 100644 --- a/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityMenuActions.tsx +++ b/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityMenuActions.tsx @@ -75,7 +75,7 @@ function EntityMenuActions(props: Props) { {menuItems.has(EntityMenuItems.DELETE) && ( )} - {menuItems.has(EntityMenuItems.RAISE_INCIDENT) && }{' '} + {menuItems.has(EntityMenuItems.RAISE_INCIDENT) && } {hasVersioningActions && ( void; + width?: number; +}; + +export function GroupBySelect({ options, selectedValue, onSelect, width = 50 }: GroupBySelectProps) { + const selectedOption = options.find((option) => option.value === selectedValue) || { label: undefined }; + + const displayValue = selectedOption.label ? `Group ${selectedOption.label}` : 'Group'; + + return ( + { + if (value.length) { + onSelect(value[0]); + } else { + onSelect(''); + } + }} + placeholder={displayValue} + size="md" + showClear={false} + width={width} + selectLabelProps={{ label: 'Group', variant: 'labeled' }} + /> + ); +} diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylAssertionsSummaryLoading.tsx b/datahub-web-react/src/app/entityV2/shared/TableLoadingSkeleton.tsx similarity index 92% rename from datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylAssertionsSummaryLoading.tsx rename to datahub-web-react/src/app/entityV2/shared/TableLoadingSkeleton.tsx index fe4912379bb96..6f2412aaef522 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylAssertionsSummaryLoading.tsx +++ b/datahub-web-react/src/app/entityV2/shared/TableLoadingSkeleton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from 'styled-components'; import { Skeleton } from 'antd'; -import { ANTD_GRAY } from '../../../constants'; +import { ANTD_GRAY } from './constants'; const Header = styled.div` width: 100%; @@ -38,7 +38,7 @@ const CardSkeleton = styled(Skeleton.Input)` } `; -export const AcrylAssertionsSummaryLoading = () => { +export const TableLoadingSkeleton = () => { return ( <>
diff --git a/datahub-web-react/src/app/entityV2/shared/components/search/InlineListSearch.tsx b/datahub-web-react/src/app/entityV2/shared/components/search/InlineListSearch.tsx new file mode 100644 index 0000000000000..c74f1ce107e3d --- /dev/null +++ b/datahub-web-react/src/app/entityV2/shared/components/search/InlineListSearch.tsx @@ -0,0 +1,45 @@ +import { SearchOutlined } from '@ant-design/icons'; +import React from 'react'; +import { pluralize } from '@src/app/shared/textUtil'; +import { MatchLabelText, SearchContainer, StyledInput } from './styledComponents'; + +interface InlineListSearchProps { + searchText: string; + debouncedSetFilterText: (event: React.ChangeEvent) => void; + matchResultCount: number; + numRows: number; + options?: { + hidePrefix?: boolean; + placeholder?: string; + allowClear?: boolean; + hideMatchCountText?: boolean; + }; + entityTypeName: string; +} + +export const InlineListSearch: React.FC = ({ + searchText, + debouncedSetFilterText, + matchResultCount, + numRows, + entityTypeName, + options, +}) => { + return ( + + } + /> + {searchText && !options?.hideMatchCountText && ( + + Matched {matchResultCount} {pluralize(matchResultCount, entityTypeName)} of {numRows} + + )} + + ); +}; diff --git a/datahub-web-react/src/app/entityV2/shared/components/ListSearch/AcrylListSearch.tsx b/datahub-web-react/src/app/entityV2/shared/components/search/styledComponents.tsx similarity index 52% rename from datahub-web-react/src/app/entityV2/shared/components/ListSearch/AcrylListSearch.tsx rename to datahub-web-react/src/app/entityV2/shared/components/search/styledComponents.tsx index 4786a73869684..8720bdf926e34 100644 --- a/datahub-web-react/src/app/entityV2/shared/components/ListSearch/AcrylListSearch.tsx +++ b/datahub-web-react/src/app/entityV2/shared/components/search/styledComponents.tsx @@ -1,11 +1,8 @@ -import { SearchOutlined } from '@ant-design/icons'; import { Input } from 'antd'; -import React from 'react'; import styled from 'styled-components'; -import { REDESIGN_COLORS } from '@src/app/entityV2/shared/constants'; -import { pluralize } from '@src/app/shared/textUtil'; +import { REDESIGN_COLORS } from '../../constants'; -const StyledInput = styled(Input)` +export const StyledInput = styled(Input)` width: auto; background: ${REDESIGN_COLORS.WHITE}; font-size: 14px; @@ -14,13 +11,13 @@ const StyledInput = styled(Input)` color: ${REDESIGN_COLORS.BODY_TEXT}; `; -const MatchLabelText = styled.span` +export const MatchLabelText = styled.span` font-size: 12px; font-weight: 400; color: ${REDESIGN_COLORS.DARK_GREY}; `; -const SearchContainer = styled.div` +export const SearchContainer = styled.div` position: relative; --antd-wave-shadow-color: transparent; flex: auto; @@ -75,44 +72,3 @@ const SearchContainer = styled.div` background: transparent; } `; - -interface AcrylListSearchProps { - searchText: string; - debouncedSetFilterText: (event: React.ChangeEvent) => void; - matchResultCount: number; - numRows: number; - options?: { - hidePrefix?: boolean; - placeholder?: string; - allowClear?: boolean; - hideMatchCountText?: boolean; - }; - entityTypeName: string; -} - -export const AcrylListSearch: React.FC = ({ - searchText, - debouncedSetFilterText, - matchResultCount, - numRows, - entityTypeName, - options, -}) => { - return ( - - } - /> - {searchText && !options?.hideMatchCountText && ( - - Matched {matchResultCount} {pluralize(matchResultCount, entityTypeName)} of {numRows} - - )} - - ); -}; diff --git a/datahub-web-react/src/app/entityV2/shared/constants.ts b/datahub-web-react/src/app/entityV2/shared/constants.ts index 59fe49fd07fa7..ebd7ff6212ff1 100644 --- a/datahub-web-react/src/app/entityV2/shared/constants.ts +++ b/datahub-web-react/src/app/entityV2/shared/constants.ts @@ -229,3 +229,5 @@ export const EDITING_DOCUMENTATION_URL_PARAM = 'editing'; export const UNKNOWN_DATA_PLATFORM = 'urn:li:dataPlatform:unknown'; export const SMART_ASSERTION_STALE_IN_DAYS = 3; + +export const TITLE_CASE_EXCEPTION_WORDS = ['of', 'the', 'in', 'on', 'and', 'a', 'an', 'to', 'for', 'at', 'by']; diff --git a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/Lineage/utils.tsx b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/Lineage/utils.tsx index 250e959e4c69d..dc6df4b148741 100644 --- a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/Lineage/utils.tsx +++ b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/Lineage/utils.tsx @@ -113,9 +113,7 @@ export const getRelatedEntitySummary = ( {type.count}{' '} {pluralize( type.count, - type.isEntityType - ? (entityRegistry.getEntityName(type.type as EntityType) as any) - : type.type, + type.isEntityType ? entityRegistry.getEntityName(type.type as EntityType) ?? '' : type.type, ).toLocaleLowerCase()} {idx < summary.types.length - 1 && <>, } diff --git a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarLogicSection.tsx b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarLogicSection.tsx index 842c5f1db08f1..fec058369164d 100644 --- a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarLogicSection.tsx +++ b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarLogicSection.tsx @@ -1,6 +1,7 @@ import CopyQuery from '@src/app/entity/shared/tabs/Dataset/Queries/CopyQuery'; import { useIsEmbeddedProfile } from '@src/app/shared/useEmbeddedProfileLinkProps'; import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { GetDataJobQuery } from '@src/graphql/dataJob.generated'; import { Button, Modal } from 'antd'; import React, { useContext, useMemo, useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; @@ -45,6 +46,16 @@ export function SidebarDatasetViewDefinitionSection() { return ; } +export function SidebarDataJobTransformationLogicSection() { + const baseEntity = useBaseEntity(); + const statement = baseEntity?.dataJob?.dataTransformLogic?.transforms?.[0]?.queryStatement?.value; + const entityRegistry = useEntityRegistry(); + const externalUrl = entityRegistry.getEntityUrl(EntityType.DataJob, baseEntity?.dataJob?.urn || ''); + if (!statement) return null; + + return ; +} + export function SidebarQueryLogicSection() { const baseEntity = useBaseEntity<{ entity: QueryEntity }>(); const statement = baseEntity?.entity?.properties?.statement?.value; diff --git a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx index fe340274b340f..9838c170067f4 100644 --- a/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx +++ b/datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarSiblingsSection.tsx @@ -1,16 +1,16 @@ +import { useIsShowSeparateSiblingsEnabled } from '@src/app/useAppConfig'; import React, { useState } from 'react'; import styled from 'styled-components'; -import { useIsShowSeparateSiblingsEnabled } from '@src/app/useAppConfig'; +import { GetDatasetQuery } from '../../../../../../graphql/dataset.generated'; +import { Dataset, Entity } from '../../../../../../types.generated'; import { useDataNotCombinedWithSiblings, useEntityData } from '../../../../../entity/shared/EntityContext'; import { stripSiblingsFromEntity } from '../../../../../entity/shared/siblingUtils'; import { CompactEntityNameList } from '../../../../../recommendations/renderer/component/CompactEntityNameList'; -import { Dataset, Entity } from '../../../../../../types.generated'; +import { UnionType } from '../../../../../searchV2/utils/constants'; +import { EmbeddedListSearchModal } from '../../../components/styled/search/EmbeddedListSearchModal'; +import { REDESIGN_COLORS } from '../../../constants'; import { SEPARATE_SIBLINGS_URL_PARAM, useIsSeparateSiblingsMode } from '../../../useIsSeparateSiblingsMode'; -import { GetDatasetQuery } from '../../../../../../graphql/dataset.generated'; import { SidebarSection } from './SidebarSection'; -import { REDESIGN_COLORS } from '../../../constants'; -import { EmbeddedListSearchModal } from '../../../components/styled/search/EmbeddedListSearchModal'; -import { UnionType } from '../../../../../searchV2/utils/constants'; const EntityListContainer = styled.div` display: flex; @@ -52,7 +52,7 @@ export const SidebarSiblingsSection = () => { title="Part of" content={ - + } /> @@ -85,11 +85,11 @@ export const SidebarSiblingsSection = () => { + {numSiblingsNotShown > 0 && ( setShowAllSiblings(true)}> diff --git a/datahub-web-react/src/app/entityV2/shared/sidebarSection/SidebarStructuredProperties.tsx b/datahub-web-react/src/app/entityV2/shared/sidebarSection/SidebarStructuredProperties.tsx index 0ce8aef32a029..86bb8464e5f9d 100644 --- a/datahub-web-react/src/app/entityV2/shared/sidebarSection/SidebarStructuredProperties.tsx +++ b/datahub-web-react/src/app/entityV2/shared/sidebarSection/SidebarStructuredProperties.tsx @@ -31,6 +31,7 @@ import { SidebarSection } from '../containers/profile/sidebar/SidebarSection'; import { StyledDivider } from '../tabs/Dataset/Schema/components/SchemaFieldDrawer/components'; import StructuredPropertyValue from '../tabs/Properties/StructuredPropertyValue'; import { PropertyRow } from '../tabs/Properties/types'; +import { useHydratedEntityMap } from '../tabs/Properties/useHydratedEntityMap'; interface FieldProperties { isSchemaSidebar?: boolean; @@ -91,6 +92,14 @@ const SidebarStructuredProperties = ({ properties }: Props) => { ? getPropertyRowFromSearchResult(selectedProperty, allProperties)?.values : undefined; + const uniqueEntityUrnsToHydrate = entityTypeProperties?.flatMap((property) => { + const propertyRow: PropertyRow | undefined = getPropertyRowFromSearchResult(property, allProperties); + const values = propertyRow?.values; + return values?.map((value) => value?.entity?.urn); + }); + + const hydratedEntityMap = useHydratedEntityMap(uniqueEntityUrnsToHydrate); + return ( <> {entityTypeProperties?.map((property) => { @@ -109,7 +118,11 @@ const SidebarStructuredProperties = ({ properties }: Props) => { {values ? ( <> {values.map((val) => ( - + ))} ) : ( diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx index aac6bab1d6c0a..b24c86e447115 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx @@ -2,6 +2,7 @@ import { CodeOutlined, ReadOutlined, UnorderedListOutlined } from '@ant-design/i import { generateSchemaFieldUrn } from '@app/entityV2/shared/tabs/Lineage/utils'; import { useGetEntitiesNotesQuery } from '@graphql/relationships.generated'; import QueryStatsOutlinedIcon from '@mui/icons-material/QueryStatsOutlined'; +import { TabRenderType } from '@src/app/entityV2/shared/types'; import { Drawer, Typography } from 'antd'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -238,6 +239,7 @@ export default function SchemaFieldDrawer({ <> {!openTimelineDrawer && ( setExpandedDrawerFieldPath(null)} getContainer={() => document.getElementById('entity-profile-sidebar') as HTMLElement} @@ -258,7 +260,10 @@ export default function SchemaFieldDrawer({ e.stopPropagation()}> {selectedTab && ( - + )} diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/StatsSidebarView.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/StatsSidebarView.tsx index 4b639326de065..a7b1a048845e7 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/StatsSidebarView.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/StatsSidebarView.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { LoadingOutlined } from '@ant-design/icons'; -import { DatasetFieldProfile, SchemaField } from '../../../../../../../../types.generated'; +import { DatasetFieldProfile, DatasetProfile, SchemaField } from '../../../../../../../../types.generated'; import StatsSidebarHeader, { StatsViewType } from './StatsSidebarHeader'; import { StatsSidebarContent } from './StatsSidebarContent'; import StatsSidebarColumnTab from './StatsSidebarColumnTab'; @@ -12,11 +12,11 @@ import { toLocalTimeString, } from '../../../../../../../shared/time/timeUtils'; -interface Props { +export interface StatsProps { properties: { expandedField: SchemaField; fieldProfile: DatasetFieldProfile | undefined; - profiles: any[]; + profiles: DatasetProfile[]; fetchDataWithLookbackWindow: (lookbackWindow: any) => void; profilesDataLoading: boolean; }; @@ -39,7 +39,7 @@ const StyledLoading = styled(LoadingOutlined)` export default function StatsSidebarView({ properties: { expandedField, fieldProfile, profiles, fetchDataWithLookbackWindow, profilesDataLoading }, -}: Props) { +}: StatsProps) { const [viewType, setViewType] = useState(StatsViewType.LATEST); const [lookbackWindow, setLookbackWindow] = useState(LOOKBACK_WINDOWS.QUARTER); diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylValidationsTab.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylValidationsTab.tsx index 81c71018f42f5..6f722453fce86 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylValidationsTab.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AcrylValidationsTab.tsx @@ -38,7 +38,7 @@ const TabToolbar = styled.div` `; const TabContentWrapper = styled.div` - @media screen and (min-height: 800px) { + @media screen and (max-height: 800px) { display: contents; overflow: auto; } diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionList.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionList.tsx index 3f51de8a96b52..c0339b70a00f2 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionList.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionList.tsx @@ -1,3 +1,4 @@ +import { TableLoadingSkeleton } from '@app/entityV2/shared/TableLoadingSkeleton'; import React, { useEffect, useState } from 'react'; import { Empty } from 'antd'; import { useGetDatasetContractQuery } from '@src/graphql/contract.generated'; @@ -8,12 +9,10 @@ import { useIsSeparateSiblingsMode } from '../../../../useIsSeparateSiblingsMode import { tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery } from '../acrylUtils'; import { combineEntityDataWithSiblings } from '../../../../../../entity/shared/siblingUtils'; import { getFilteredTransformedAssertionData } from './utils'; -import { AcrylAssertionsSummaryLoading } from '../AcrylAssertionsSummaryLoading'; import { AssertionTable, AssertionListFilter } from './types'; import { AssertionListTitleContainer } from './AssertionListTitleContainer'; import { AcrylAssertionListFilters } from './AcrylAssertionListFilters'; import { AcrylAssertionListTable } from './AcrylAssertionListTable'; -import { useSetFilterFromURLParams } from './hooks'; import { ASSERTION_DEFAULT_FILTERS, ASSERTION_DEFAULT_RAW_DATA } from './constant'; /** @@ -28,7 +27,6 @@ export const AcrylAssertionList = () => { }); // TODO we need to create setter function to set the filter as per the filter component const [filter, setFilters] = useState(ASSERTION_DEFAULT_FILTERS); - useSetFilterFromURLParams(filter, setFilters); const [assertionMonitorData, setAssertionMonitorData] = useState([]); @@ -68,7 +66,7 @@ export const AcrylAssertionList = () => { const renderListTable = () => { if (loading) { - return ; + return ; } if ((visibleAssertions?.assertions || []).length > 0) { return ( diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionListFilters.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionListFilters.tsx index 9ca8030907b54..2a13c869aab3a 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionListFilters.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/AcrylAssertionListFilters.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { AcrylListSearch } from '@src/app/entityV2/shared/components/ListSearch/AcrylListSearch'; +import { GroupBySelect } from '@src/app/entityV2/shared/GroupBySelect'; +import { InlineListSearch } from '@src/app/entityV2/shared/components/search/InlineListSearch'; import { AcrylAssertionRecommendedFilters } from './AcrylAssertionRecommendedFilters'; -import { AcryAssertionTypeSelect } from './AcryAssertionTypeSelect'; import { AssertionListFilter, AssertionTable } from './types'; import { AcrylAssertionFilters } from './AcrylAssertionFilters'; import { ASSERTION_GROUP_BY_FILTER_OPTIONS, ASSERTION_DEFAULT_FILTERS } from './constant'; +import { useSetFilterFromURLParams } from './hooks'; interface FilterItem { name: string; @@ -106,11 +107,14 @@ export const AcrylAssertionListFilters: React.FC // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter, filterOptions]); + // set the filter if there is any url filter object presents + useSetFilterFromURLParams(filter, setFilters); + return ( <> {/* ************Render Search Component ************************* */} - {/* ************Render Group By Component ************************* */}
-
diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/Summary/AcrylAssertionSummarySection.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/Summary/AcrylAssertionSummarySection.tsx index 285d840df2188..ff5914ed50f75 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/Summary/AcrylAssertionSummarySection.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/AssertionList/Summary/AcrylAssertionSummarySection.tsx @@ -52,6 +52,7 @@ export const AcrylAssertionSummarySection: React.FC = ({ gr )}/Quality/List${buildAssertionUrlSearch({ type: group.type, status: status.resultType })}`; return ( @@ -68,9 +69,11 @@ export const AcrylAssertionSummarySection: React.FC = ({ gr } > - - {group.summary[key]} {status.text} - + event.stopPropagation()}> + + {group.summary[key]} {status.text} + {' '} + ); diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/acrylUtils.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/acrylUtils.tsx index 49199c4b071a2..0b4d8e2afcab8 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/acrylUtils.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/acrylUtils.tsx @@ -17,6 +17,7 @@ import { sortAssertions } from './assertionUtils'; import { AssertionGroup, AssertionStatusSummary } from './acrylTypes'; import { lowerFirstLetter } from '../../../../../shared/textUtil'; import { GenericEntityProperties } from '../../../../../entity/shared/types'; +import { toProperTitleCase } from '../../../utils'; export const SUCCESS_COLOR_HEX = '#52C41A'; export const FAILURE_COLOR_HEX = '#F5222D'; @@ -140,7 +141,7 @@ ASSERTION_INFO.forEach((info) => { }); export const getAssertionGroupName = (type: string): string => { - return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).name : type; + return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).name : toProperTitleCase(type); }; export const getAssertionGroupTypeIcon = (type: string) => { diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/actions/ActionItem.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/actions/ActionItem.tsx index 81a5e20da9229..8b6dcc9870b83 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/actions/ActionItem.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/assertion/profile/actions/ActionItem.tsx @@ -21,6 +21,7 @@ type Props = { placement?: TooltipPlacement; isExpandedView?: boolean; actionName?: string; + dataTestId?: string; }; export const ActionItem = ({ @@ -33,6 +34,7 @@ export const ActionItem = ({ placement = 'top', isExpandedView = false, actionName, + dataTestId, }: Props) => { return ( @@ -50,6 +52,7 @@ export const ActionItem = ({ disabled={disabled} title={!isExpandedView ? tip : undefined} isExpandedView={isExpandedView} + data-testid={dataTestId} > {icon} diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/PropertiesTab.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/PropertiesTab.tsx index 4e816a11f8e43..6a86e1930ea0e 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/PropertiesTab.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/PropertiesTab.tsx @@ -12,9 +12,11 @@ import { mapCustomPropertiesToPropertyRows, } from '../../../../entity/shared/tabs/Properties/utils'; import { useEntityRegistryV2 } from '../../../../useEntityRegistry'; +import { TabRenderType } from '../../types'; import ExpandIcon from '../Dataset/Schema/components/ExpandIcon'; import NameColumn from './NameColumn'; import ValuesColumn from './ValuesColumn'; +import { useHydratedEntityMap } from './useHydratedEntityMap'; import useStructuredProperties from './useStructuredProperties'; const StyledTable = styled(Table)` @@ -40,9 +42,10 @@ interface Props { fieldProperties?: Maybe; refetch?: () => void; }; + renderType?: TabRenderType; } -export const PropertiesTab = ({ properties }: Props) => { +export const PropertiesTab = ({ renderType = TabRenderType.DEFAULT, properties }: Props) => { const fieldPath = properties?.fieldPath; const fieldUrn = properties?.fieldUrn; const fieldProperties = properties?.fieldProperties; @@ -50,6 +53,30 @@ export const PropertiesTab = ({ properties }: Props) => { const [filterText, setFilterText] = useState(''); const { entityData } = useEntityData(); const entityRegistry = useEntityRegistryV2(); + + const { structuredPropertyRows, expandedRowsFromFilter, structuredPropertyRowsRaw } = useStructuredProperties( + entityRegistry, + fieldPath || null, + filterText, + ); + + // only show entity custom properties on entity level, not on field level + const customProperties = !fieldPath ? getFilteredCustomProperties(filterText, entityData) || [] : []; + const customPropertyRows = mapCustomPropertiesToPropertyRows(customProperties); + const dataSource: PropertyRow[] = structuredPropertyRows + .concat(customPropertyRows) + .filter((row) => !row.structuredProperty?.settings?.isHidden); + + const [expandedRows, setExpandedRows] = useState>(new Set()); + + useUpdateExpandedRowsFromFilter({ expandedRowsFromFilter, setExpandedRows }); + + const entityUrnsToHydrate = structuredPropertyRowsRaw + .flatMap((row) => row?.values?.map((v) => (typeof v?.value === 'string' ? v.value : null))) + .filter(Boolean); + + const hydratedEntityMap = useHydratedEntityMap(entityUrnsToHydrate); + const propertyTableColumns = [ { width: 210, @@ -59,7 +86,14 @@ export const PropertiesTab = ({ properties }: Props) => { { title: 'Value', ellipsis: true, - render: (propertyRow: PropertyRow) => , + render: (propertyRow: PropertyRow) => ( + + ), }, ]; @@ -81,23 +115,6 @@ export const PropertiesTab = ({ properties }: Props) => { } as any); } - const { structuredPropertyRows, expandedRowsFromFilter } = useStructuredProperties( - entityRegistry, - fieldPath || null, - filterText, - ); - - // only show entity custom properties on entity level, not on field level - const customProperties = !fieldPath ? getFilteredCustomProperties(filterText, entityData) || [] : []; - const customPropertyRows = mapCustomPropertiesToPropertyRows(customProperties); - const dataSource: PropertyRow[] = structuredPropertyRows - .concat(customPropertyRows) - .filter((row) => !row.structuredProperty?.settings?.isHidden); - - const [expandedRows, setExpandedRows] = useState>(new Set()); - - useUpdateExpandedRowsFromFilter({ expandedRowsFromFilter, setExpandedRows }); - return ( <> ; } export default function StructuredPropertyValue({ @@ -75,6 +77,7 @@ export default function StructuredPropertyValue({ truncateText, isFieldColumn, size = 12, + hydratedEntityMap, }: Props) { const entityRegistry = useEntityRegistry(); @@ -83,9 +86,14 @@ export default function StructuredPropertyValue({ ? getSchemaFieldParentLink(entity.urn) : entityRegistry.getEntityUrl(entity.type, entity.urn); - return ( - - {value.entity ? ( + let valueEntityRender = <>; + if (value.entity) { + if (hydratedEntityMap && hydratedEntityMap[value.entity.urn]) { + valueEntityRender = ( + + ); + } else { + valueEntityRender = ( @@ -97,6 +105,14 @@ export default function StructuredPropertyValue({ + ); + } + } + + return ( + + {value.entity ? ( + valueEntityRender ) : ( <> {isRichText ? ( diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/ValuesColumn.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/ValuesColumn.tsx index b2604092fec46..41fbe3ecac585 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/ValuesColumn.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/ValuesColumn.tsx @@ -1,26 +1,51 @@ +import { Entity } from '@src/types.generated'; import React from 'react'; +import styled from 'styled-components'; import { StdDataType } from '../../../../../types.generated'; +import { TabRenderType } from '../../types'; import StructuredPropertyValue from './StructuredPropertyValue'; import { PropertyRow } from './types'; interface Props { propertyRow: PropertyRow; filterText?: string; + hydratedEntityMap?: Record; + renderType: TabRenderType; } -export default function ValuesColumn({ propertyRow, filterText }: Props) { +const ValuesContainerFlex = styled.div<{ renderType: TabRenderType }>` + display: flex; + flex-direction: ${(props) => (props.renderType === TabRenderType.COMPACT ? 'column' : 'row')}; + gap: 5px; + justify-content: flex-start; + align-items: flex-start; /* Ensure items are aligned at the start */ + flex-wrap: wrap; +`; + +const ValueContainer = styled.div` + flex: 0 1 auto; +`; + +export default function ValuesColumn({ propertyRow, filterText, hydratedEntityMap, renderType }: Props) { const { values } = propertyRow; const isRichText = propertyRow.dataType?.info?.type === StdDataType.RichText; return ( - <> + {values ? ( values.map((v) => ( - + + + )) ) : ( )} - + ); } diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useHydratedEntityMap.ts b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useHydratedEntityMap.ts new file mode 100644 index 0000000000000..b0556311442c2 --- /dev/null +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useHydratedEntityMap.ts @@ -0,0 +1,26 @@ +import { useGetEntities } from '@src/app/sharedV2/useGetEntities'; +import { Entity } from '@src/types.generated'; +import { useMemo } from 'react'; + +export function useHydratedEntityMap(urns?: (string | undefined | null)[]) { + // Get unique URNs + const uniqueEntityUrns = useMemo( + () => Array.from(new Set(urns?.filter((urn): urn is string => !!urn) || [])), + [urns], + ); + + // Fetch entities + const hydratedEntities = useGetEntities(uniqueEntityUrns); + + // Create entity map + const hydratedEntityMap = useMemo( + () => + hydratedEntities.reduce>((acc, entity) => { + acc[entity.urn] = entity; + return acc; + }, {}), + [hydratedEntities], + ); + + return hydratedEntityMap; +} diff --git a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useStructuredProperties.tsx b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useStructuredProperties.tsx index b56b4cd35640c..ac53c71a2f9cd 100644 --- a/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useStructuredProperties.tsx +++ b/datahub-web-react/src/app/entityV2/shared/tabs/Properties/useStructuredProperties.tsx @@ -248,5 +248,6 @@ export default function useStructuredProperties( return { structuredPropertyRows, expandedRowsFromFilter: expandedRowsFromFilter as Set, + structuredPropertyRowsRaw, }; } diff --git a/datahub-web-react/src/app/entityV2/shared/utils.ts b/datahub-web-react/src/app/entityV2/shared/utils.ts index 53e2b48398f3e..0ff2b418e1683 100644 --- a/datahub-web-react/src/app/entityV2/shared/utils.ts +++ b/datahub-web-react/src/app/entityV2/shared/utils.ts @@ -13,12 +13,14 @@ import { DatasetProperties, ChartProperties, Operation, + Dataset, } from '../../../types.generated'; import { capitalizeFirstLetterOnly } from '../../shared/textUtil'; import { GenericEntityProperties } from '../../entity/shared/types'; import { OUTPUT_PORTS_FIELD } from '../../search/utils/constants'; import { TimeWindowSize } from '../../shared/time/timeUtils'; +import { TITLE_CASE_EXCEPTION_WORDS } from './constants'; export function dictToQueryStringParams(params: Record) { return Object.keys(params) @@ -294,3 +296,34 @@ export function getDashboardLastUpdatedMs( if (max === lastModified) return { property: 'lastModified', lastUpdatedMs: lastModified }; return { property: 'lastRefreshed', lastUpdatedMs: lastRefreshed }; } + +// return title case of the string with handling exceptions +export const toProperTitleCase = (str: string) => { + return str + .toLowerCase() + .split(' ') + .map((word, index) => + index === 0 || !TITLE_CASE_EXCEPTION_WORDS.includes(word) + ? word.charAt(0).toUpperCase() + word.slice(1) + : word, + ) + .join(' '); +}; + +/** + * Attempts to extract a description for a sub-resource of an entity, if it exists. + * @param entity ie dataset + * @param subResource ie field name + * @returns the description of the sub-resource if it exists, otherwise undefined + */ +export const tryExtractSubResourceDescription = (entity: Entity, subResource: string): string | undefined => { + // NOTE: we are casting to Dataset, but GlossaryTerms and more future entities can have editableSchemaMetadata + // We must do a ? check for editableSchemaMetadata/schemaMetadata to avoid runtime errors + const maybeEditableMetadataDescription = (entity as Dataset).editableSchemaMetadata?.editableSchemaFieldInfo?.find( + (field) => field.fieldPath === subResource, + )?.description; + const maybeSchemaMetadataDescription = (entity as Dataset).schemaMetadata?.fields?.find( + (field) => field.fieldPath === subResource, + )?.description; + return maybeEditableMetadataDescription?.valueOf() || maybeSchemaMetadataDescription?.valueOf(); +}; diff --git a/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/insight/cards/PopularGlossaryTerms.tsx b/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/insight/cards/PopularGlossaryTerms.tsx index 8a8558fe361e8..b1b771b0f65ed 100644 --- a/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/insight/cards/PopularGlossaryTerms.tsx +++ b/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/insight/cards/PopularGlossaryTerms.tsx @@ -63,7 +63,7 @@ export const PopularGlossaryTerms = () => { limit: 10, }, }, - fetchPolicy: 'no-cache', + fetchPolicy: 'cache-first', skip: !userUrn, }); const recommendationModules = data?.listRecommendations?.modules; diff --git a/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx b/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx index 9785fc47a921f..5a9a5de8966f9 100644 --- a/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx +++ b/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx @@ -56,8 +56,14 @@ const ListContainer = styled.div``; enum TabType { Sources = 'Sources', Secrets = 'Secrets', + RemoteExecutors = 'Executors', } +const TabTypeToListComponent = { + [TabType.Sources]: , + [TabType.Secrets]: , +}; + export const ManageIngestionPage = () => { /** * Determines which view should be visible: ingestion sources or secrets. @@ -78,7 +84,8 @@ export const ManageIngestionPage = () => { }, [loaded, me.loaded, showIngestionTab, selectedTab]); const onClickTab = (newTab: string) => { - setSelectedTab(TabType[newTab]); + const matchingTab = Object.values(TabType).find((tab) => tab === newTab); + setSelectedTab(matchingTab || selectedTab); }; return ( @@ -94,7 +101,7 @@ export const ManageIngestionPage = () => { {showIngestionTab && } {showSecretsTab && } - {selectedTab === TabType.Sources ? : } + {TabTypeToListComponent[selectedTab]} ); }; diff --git a/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTable.tsx b/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTable.tsx index 2fef00559d06a..da580cdfc25ff 100644 --- a/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTable.tsx +++ b/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTable.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { Empty, Pagination, Typography } from 'antd'; import styled from 'styled-components'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; import { StyledTable } from '../../../entity/shared/components/styled/StyledTable'; -import { ExecutionRequest } from '../../../../types.generated'; +import { EntityType, ExecutionRequest } from '../../../../types.generated'; import { ButtonsColumn, SourceColumn, StatusColumn, TimeColumn } from './IngestionExecutionTableColumns'; import { SUCCESS, getIngestionSourceStatus } from '../utils'; import { formatDuration } from '../../../shared/formatDuration'; @@ -54,6 +55,7 @@ export default function IngestionExecutionTable({ lastResultIndex, page, }: Props) { + const entityRegistry = useEntityRegistry(); const tableColumns = [ { title: 'Requested At', @@ -85,8 +87,18 @@ export default function IngestionExecutionTable({ title: 'Source', dataIndex: 'source', key: 'source', - render: SourceColumn, + render: (_, record: any) => ( + + ), }, + { title: '', dataIndex: '', @@ -107,6 +119,7 @@ export default function IngestionExecutionTable({ const tableData = executionRequests.map((execution) => ({ urn: execution.urn, + actorUrn: execution.input.actorUrn, id: execution.id, source: execution.input.source.type, requestedAt: execution.input?.requestedAt, diff --git a/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTableColumns.tsx b/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTableColumns.tsx index 5bf96bc703f1f..2cb1eee0ad3e4 100644 --- a/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTableColumns.tsx +++ b/datahub-web-react/src/app/ingest/source/executions/IngestionExecutionTableColumns.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { CopyOutlined } from '@ant-design/icons'; -import { Button, Typography, Tooltip } from 'antd'; +import { Button, Typography } from 'antd'; +import { Text, Tooltip } from '@components'; import styled from 'styled-components'; +import CustomAvatar from '@src/app/shared/avatar/CustomAvatar'; +import { Link } from 'react-router-dom'; +import { CreatedByContainer } from '@src/app/govern/structuredProperties/styledComponents'; import { getExecutionRequestStatusDisplayColor, getExecutionRequestStatusIcon, @@ -13,6 +17,19 @@ import { SUCCESS, } from '../utils'; +type Actor = { + actorUrn: string; + displayName: string; + displayUrl: string; +}; + +const UserContainer = styled.div` + display: flex; + align-items: center; + gap: 4px; + wrap: auto; +`; + const StatusContainer = styled.div` display: flex; justify-content: left; @@ -52,13 +69,42 @@ export function StatusColumn({ status, record, setFocusExecutionUrn }: StatusCol ); } -export function SourceColumn(source: string) { - return ( - (source === MANUAL_INGESTION_SOURCE && 'Manual Execution') || - (source === SCHEDULED_INGESTION_SOURCE && 'Scheduled Execution') || - (source === CLI_INGESTION_SOURCE && 'CLI Execution') || - 'N/A' - ); +// TODO: This will be soon exported into a component +const UserPill = ({ actor }: { actor: Actor }) => ( + + + + + {actor?.displayName} + + + +); + +interface SourceColumnProps { + source: string; + actor?: Actor; +} + +export function SourceColumn({ source, actor }: SourceColumnProps) { + switch (source) { + case MANUAL_INGESTION_SOURCE: + if (!actor) return <>Manual Execution; + return ( + + Manual Execution by + + ); + + case SCHEDULED_INGESTION_SOURCE: + return Scheduled Execution; + + case CLI_INGESTION_SOURCE: + return CLI Execution; + + default: + return N/A; + } } interface ButtonsColumnProps { diff --git a/datahub-web-react/src/app/previewV2/PreviewCardFooterRightSection.tsx b/datahub-web-react/src/app/previewV2/PreviewCardFooterRightSection.tsx index 349a0b0dfa874..cb9f082b88842 100644 --- a/datahub-web-react/src/app/previewV2/PreviewCardFooterRightSection.tsx +++ b/datahub-web-react/src/app/previewV2/PreviewCardFooterRightSection.tsx @@ -67,8 +67,10 @@ const PreviewCardFooterRightSection = ({ {showLineageBadge && ( <> ` + display: inline-flex; + align-items: center; + max-width: 100%; + ${(props) => props.addMargin && 'margin: 2px 0;'} +`; + +const StyledArrow = styled(ArrowRightOutlined)` + color: ${ANTD_GRAY[8]}; + margin: 0 4px; +`; + +type CompactEntityNameProps = { + entity: Entity; + showFullTooltip?: boolean; + showArrow?: boolean; + placement?: TooltipPlacement; + onClick?: () => void; + linkUrlParams?: Record; +}; + +export const CompactEntityNameComponent = ({ + entity, + showFullTooltip = true, + showArrow = false, + placement, + onClick, + linkUrlParams, +}: CompactEntityNameProps) => { + const entityRegistry = useEntityRegistry(); + + if (!entity) return null; + + let processedEntity = entity; + let columnName; + + if (entity.type === EntityType.SchemaField) { + const { parent, fieldPath } = entity as SchemaFieldEntity; + processedEntity = parent; + columnName = fieldPath; + } + + const genericProps = entityRegistry.getGenericEntityProperties(processedEntity.type, processedEntity); + const platformLogoUrl = genericProps?.platform?.properties?.logoUrl; + const displayName = entityRegistry.getDisplayName(processedEntity.type, processedEntity); + const fallbackIcon = entityRegistry.getIcon(processedEntity.type, 12, IconStyleType.ACCENT); + const url = entityRegistry.getEntityUrl(processedEntity.type, processedEntity.urn, linkUrlParams); + + return ( + + + + platform.properties?.logoUrl, + )} + logoComponent={fallbackIcon} + onClick={onClick} + columnName={columnName} + dataTestId={`compact-entity-link-${processedEntity.urn}`} + /> + + + {showArrow && } + + ); +}; diff --git a/datahub-web-react/src/app/recommendations/renderer/component/CompactEntityNameList.tsx b/datahub-web-react/src/app/recommendations/renderer/component/CompactEntityNameList.tsx index ec84ec6c3bfb0..d918b4458c17e 100644 --- a/datahub-web-react/src/app/recommendations/renderer/component/CompactEntityNameList.tsx +++ b/datahub-web-react/src/app/recommendations/renderer/component/CompactEntityNameList.tsx @@ -1,88 +1,38 @@ -import { ArrowRightOutlined } from '@ant-design/icons'; -import React from 'react'; -import styled from 'styled-components/macro'; +import { Entity } from '@src/types.generated'; import { TooltipPlacement } from 'antd/es/tooltip'; -import { Entity, EntityType, SchemaFieldEntity } from '../../../../types.generated'; -import { IconStyleType } from '../../../entity/Entity'; -import { useEntityRegistry } from '../../../useEntityRegistry'; -import { EntityPreviewTag } from './EntityPreviewTag'; -import { HoverEntityTooltip } from './HoverEntityTooltip'; -import { ANTD_GRAY } from '../../../entity/shared/constants'; - -const NameWrapper = styled.span<{ addMargin }>` - display: inline-flex; - align-items: center; - max-width: 100%; - ${(props) => props.addMargin && 'margin: 2px 0;'} -`; - -const StyledArrow = styled(ArrowRightOutlined)` - color: ${ANTD_GRAY[8]}; - margin: 0 4px; -`; +import React from 'react'; +import { CompactEntityNameComponent } from './CompactEntityNameComponent'; -type Props = { +type CompactEntityNameListProps = { entities: Array; onClick?: (index: number) => void; linkUrlParams?: Record; - showTooltips?: boolean; + showFullTooltips?: boolean; showArrows?: boolean; placement?: TooltipPlacement; }; + export const CompactEntityNameList = ({ entities, onClick, linkUrlParams, - showTooltips = true, + showFullTooltips = false, showArrows, placement, -}: Props) => { - const entityRegistry = useEntityRegistry(); - +}: CompactEntityNameListProps) => { return ( <> - {entities.map((mappedEntity, index) => { - if (!mappedEntity) return <>; - let entity = mappedEntity; - let columnName; - if (entity.type === EntityType.SchemaField) { - const { parent, fieldPath } = entity as SchemaFieldEntity; - entity = parent; - columnName = fieldPath; - } - - const isLastEntityInList = index === entities.length - 1; - const showArrow = showArrows && !isLastEntityInList; - const genericProps = entityRegistry.getGenericEntityProperties(entity.type, entity); - const platformLogoUrl = genericProps?.platform?.properties?.logoUrl; - const displayName = entityRegistry.getDisplayName(entity.type, entity); - const fallbackIcon = entityRegistry.getIcon(entity.type, 12, IconStyleType.ACCENT); - const url = entityRegistry.getEntityUrl(entity.type, entity.urn, linkUrlParams); - return ( - - - platform.properties?.logoUrl, - )} - logoComponent={fallbackIcon} - onClick={() => onClick?.(index)} - columnName={columnName} - dataTestId={`compact-entity-link-${entity.urn}`} - /> - - {showArrow && } - - ); - })} + {entities.map((entity, index) => ( + onClick?.(index)} + linkUrlParams={linkUrlParams} + /> + ))} ); }; diff --git a/datahub-web-react/src/app/recommendations/renderer/component/EntityPreviewTag.tsx b/datahub-web-react/src/app/recommendations/renderer/component/EntityPreviewTag.tsx index 489b33630bb20..3d7c57ca20093 100644 --- a/datahub-web-react/src/app/recommendations/renderer/component/EntityPreviewTag.tsx +++ b/datahub-web-react/src/app/recommendations/renderer/component/EntityPreviewTag.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import { Divider, Image, Tag } from 'antd'; import { Tooltip } from '@components'; -import styled from 'styled-components'; -import { Link } from 'react-router-dom'; -import { Maybe } from 'graphql/jsutils/Maybe'; import { useEmbeddedProfileLinkProps } from '@src/app/shared/useEmbeddedProfileLinkProps'; +import { Divider, Image, Tag } from 'antd'; +import { Maybe } from 'graphql/jsutils/Maybe'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; import { ANTD_GRAY } from '../../../entity/shared/constants'; const EntityTag = styled(Tag)` @@ -69,6 +69,7 @@ type Props = { onClick?: () => void; columnName?: string; dataTestId?: string; + showNameTooltip?: boolean; }; const constructExternalUrl = (url) => { @@ -89,6 +90,7 @@ export const EntityPreviewTag = ({ onClick, columnName, dataTestId, + showNameTooltip = true, }: Props) => { const externalUrl = constructExternalUrl(url); const linkProps = useEmbeddedProfileLinkProps(); @@ -110,11 +112,11 @@ export const EntityPreviewTag = ({ logoComponent} - + {displayName} {columnName && ( - + {columnName} diff --git a/datahub-web-react/src/app/searchV2/filters/value/EntityValueMenu.tsx b/datahub-web-react/src/app/searchV2/filters/value/EntityValueMenu.tsx index d370f6bcf5f45..763c77bb34a3c 100644 --- a/datahub-web-react/src/app/searchV2/filters/value/EntityValueMenu.tsx +++ b/datahub-web-react/src/app/searchV2/filters/value/EntityValueMenu.tsx @@ -28,8 +28,7 @@ export default function EntityValueMenu({ className, }: Props) { const entityRegistry = useEntityRegistry(); - const isSearchable = - field.entityTypes?.length && field.entityTypes.every((t) => entityRegistry.getEntity(t).isSearchEnabled()); + const isSearchable = !!field.entityTypes?.length; const { displayName } = field; // Ideally we would not have staged values, and filters would update automatically. @@ -43,7 +42,8 @@ export default function EntityValueMenu({ const finalSearchOptions = [...localSearchOptions, ...deduplicateOptions(localSearchOptions, searchOptions)]; // Compute the final options to show to the user. - const finalOptions = searchQuery ? finalSearchOptions : defaultOptions; + const finalDefaultOptions = defaultOptions.length ? defaultOptions : searchOptions; + const finalOptions = searchQuery ? finalSearchOptions : finalDefaultOptions; // Finally, create the option set. // TODO: Add an option set for "no x". diff --git a/datahub-web-react/src/app/searchV2/filters/value/utils.tsx b/datahub-web-react/src/app/searchV2/filters/value/utils.tsx index 1cdde2b052f98..da49e28e5b171 100644 --- a/datahub-web-react/src/app/searchV2/filters/value/utils.tsx +++ b/datahub-web-react/src/app/searchV2/filters/value/utils.tsx @@ -5,6 +5,7 @@ import { EntityRegistry } from '../../../../entityRegistryContext'; import { useAggregateAcrossEntitiesQuery, useGetAutoCompleteMultipleResultsQuery, + useGetSearchResultsForMultipleQuery, } from '../../../../graphql/search.generated'; import { EntityType } from '../../../../types.generated'; import { capitalizeFirstLetterOnly } from '../../../shared/textUtil'; @@ -94,6 +95,19 @@ export const useLoadSearchOptions = (field: EntityFilterField, query?: string, s fetchPolicy: 'cache-first', }); + // do initial search to get initial data to display + const { data: searchData, loading: searchLoading } = useGetSearchResultsForMultipleQuery({ + skip: skip || !!query, // only do a search if not doing auto-complete, + variables: { + input: { + query: '*', + types: field.entityTypes, + count: 10, + }, + }, + fetchPolicy: 'cache-first', + }); + if (skip) { return { loading: false, options: [] }; } @@ -107,7 +121,13 @@ export const useLoadSearchOptions = (field: EntityFilterField, query?: string, s icon: field.icon, }; }); - return { options: options || [], loading }; + const searchOptions = searchData?.searchAcrossEntities?.searchResults.map((result) => ({ + value: result.entity.urn, + entity: result.entity, + icon: field.icon, + })); + + return { options: options || searchOptions || [], loading: loading || searchLoading }; }; /** diff --git a/datahub-web-react/src/app/shared/button/styledComponents.ts b/datahub-web-react/src/app/shared/button/styledComponents.ts new file mode 100644 index 0000000000000..a181aa5134dd5 --- /dev/null +++ b/datahub-web-react/src/app/shared/button/styledComponents.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const ModalButtonContainer = styled.div` + display: flex; + align-items: center; + justify-content: end; + gap: 16px; +`; diff --git a/datahub-web-react/src/app/shared/formatNumber.ts b/datahub-web-react/src/app/shared/formatNumber.ts index 8f863505326df..c48dc5cfa8e1e 100644 --- a/datahub-web-react/src/app/shared/formatNumber.ts +++ b/datahub-web-react/src/app/shared/formatNumber.ts @@ -10,16 +10,16 @@ export function formatNumberWithoutAbbreviation(n) { return n.toLocaleString(); } -export function formatBytes(bytes: number, decimals = 2): { number: number; unit: string } { +export function formatBytes(bytes: number, decimals = 2, bytesUnit = 'Bytes'): { number: number; unit: string } { if (!bytes) return { number: 0, - unit: 'Bytes', + unit: bytesUnit, }; const k = 1000; // We use IEEE standards definition of units of byte, where 1000 bytes = 1kb. const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const sizes = [bytesUnit, 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return { diff --git a/datahub-web-react/src/app/shared/textUtil.ts b/datahub-web-react/src/app/shared/textUtil.ts index e5c104708dcd3..bbadb9f34c85e 100644 --- a/datahub-web-react/src/app/shared/textUtil.ts +++ b/datahub-web-react/src/app/shared/textUtil.ts @@ -41,8 +41,8 @@ export function pluralizeIfIrregular(noun: string, suffix = 's'): string { query: 'queries', }; - if (irregularPlurals.hasOwnProperty(noun.toLowerCase())) { - return irregularPlurals[noun.toLowerCase()]; + if (irregularPlurals.hasOwnProperty(noun?.toLowerCase())) { + return irregularPlurals[noun?.toLowerCase()]; } return `${noun}${suffix}`; } diff --git a/datahub-web-react/src/app/shared/time/timeUtils.tsx b/datahub-web-react/src/app/shared/time/timeUtils.tsx index 717edf9e63d34..7dee14cdf1dda 100644 --- a/datahub-web-react/src/app/shared/time/timeUtils.tsx +++ b/datahub-web-react/src/app/shared/time/timeUtils.tsx @@ -25,7 +25,7 @@ export const INTERVAL_TO_MS = { [DateInterval.Year]: 31536000000, }; -export const INTERVAL_TO_MOMENT_INTERVAL = { +export const INTERVAL_TO_MOMENT_INTERVAL: { [key: string]: moment.DurationInputArg2 } = { [DateInterval.Second]: 'seconds', [DateInterval.Minute]: 'minutes', [DateInterval.Hour]: 'hours', @@ -83,9 +83,13 @@ export const getTimeWindowStart = (endTimeMillis: number, interval: DateInterval * @param windowSize the */ export const getFixedLookbackWindow = (windowSize: TimeWindowSize): TimeWindow => { - const endTime = Date.now(); + const endTime = moment().valueOf(); + const startTime = moment(endTime) + .subtract(windowSize.count, INTERVAL_TO_MOMENT_INTERVAL[windowSize.interval]) + .valueOf(); + return { - startTime: endTime - getTimeWindowSizeMs(windowSize), + startTime, endTime, }; }; diff --git a/datahub-web-react/src/graphql/dataset.graphql b/datahub-web-react/src/graphql/dataset.graphql index b3122332da330..89164869ba1d9 100644 --- a/datahub-web-react/src/graphql/dataset.graphql +++ b/datahub-web-react/src/graphql/dataset.graphql @@ -27,6 +27,14 @@ query getDataProfiles($urn: String!, $limit: Int, $startTime: Long, $endTime: Lo median stdev sampleValues + quantiles { + quantile + value + } + distinctValueFrequencies { + value + frequency + } } } } diff --git a/datahub-web-react/src/graphql/fragments.graphql b/datahub-web-react/src/graphql/fragments.graphql index 513857b8b4f3c..04ab566e9d27a 100644 --- a/datahub-web-react/src/graphql/fragments.graphql +++ b/datahub-web-react/src/graphql/fragments.graphql @@ -593,6 +593,9 @@ fragment dataJobFields on DataJob { activeIncidents: incidents(start: 0, count: 1, state: ACTIVE) { total } + dataTransformLogic { + ...dataTransformLogicFields + } } fragment dashboardFields on Dashboard { @@ -1441,6 +1444,9 @@ fragment entityDisplayNameFields on Entity { qualifiedName } } + ... on SchemaFieldEntity { + fieldPath + } ... on OwnershipTypeEntity { info { name @@ -1787,6 +1793,14 @@ fragment datasetProfileFields on DatasetProfile { median stdev sampleValues + quantiles { + quantile + value + } + distinctValueFrequencies { + value + frequency + } } } @@ -1819,3 +1833,12 @@ fragment notes on Entity { } } } + +fragment dataTransformLogicFields on DataTransformLogic { + transforms { + queryStatement { + value + language + } + } +} diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index c36e3038adca5..89ef2709504d5 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -1053,11 +1053,17 @@ fragment searchResultFieldsNoLineage on Entity { fragment searchResultFields on Entity { ...searchResultFieldsNoLineage ... on EntityWithRelationships { - upstream: lineage(input: { direction: UPSTREAM, start: 0, count: 0 }) { + upstream: lineage(input: { direction: UPSTREAM, start: 0, count: 100 }) { total + filtered + # avoid fetching data about results, this query is meant to give a count upstream and downstream + # count is 100 in order to allow us to filter out ghost entities and show a more accurate count in the UI } - downstream: lineage(input: { direction: DOWNSTREAM, start: 0, count: 0 }) { + downstream: lineage(input: { direction: DOWNSTREAM, start: 0, count: 100 }) { total + filtered + # avoid fetching data about results, this query is meant to give a count upstream and downstream + # count is 100 in order to allow us to filter out ghost entities and show a more accurate count in the UI } } } diff --git a/datahub-web-react/vite.config.ts b/datahub-web-react/vite.config.ts index c43470dee031a..8f13bcf2c7546 100644 --- a/datahub-web-react/vite.config.ts +++ b/datahub-web-react/vite.config.ts @@ -97,11 +97,11 @@ export default defineConfig(({ mode }) => { css: true, // reporters: ['verbose'], coverage: { - enabled: true, - provider: 'v8', + enabled: true, + provider: 'v8', reporter: ['text', 'json', 'html'], include: ['src/**/*'], - reportsDirectory: '../build/coverage-reports/datahub-web-react/', + reportsDirectory: '../build/coverage-reports/datahub-web-react/', exclude: [], }, }, diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index ee5e1b8f2f2aa..8d4560c375ce4 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -4276,6 +4276,13 @@ dependencies: "@types/d3-path" "^1" +"@types/d3-shape@^1.3.2": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.12.tgz#8f2f9f7a12e631ce6700d6d55b84795ce2c8b259" + integrity sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q== + dependencies: + "@types/d3-path" "^1" + "@types/d3-time-format@*": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" @@ -4913,6 +4920,15 @@ "@types/react-dom" "*" prop-types "^15.5.10" +"@visx/bounds@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@visx/bounds/-/bounds-3.12.0.tgz#c733bb6b1328ab82a0ab029bce4851f198f551c1" + integrity sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ== + dependencies: + "@types/react" "*" + "@types/react-dom" "*" + prop-types "^15.5.10" + "@visx/curve@3.0.0", "@visx/curve@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@visx/curve/-/curve-3.0.0.tgz#c54568472e00a38483c58cf52e4a6ddb2887c2d4" @@ -5111,6 +5127,19 @@ lodash "^4.17.21" prop-types "^15.5.10" +"@visx/stats@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@visx/stats/-/stats-3.12.0.tgz#c3589860c18b0856016a7539a45d2ff4bb2f5b31" + integrity sha512-gPc8hF/JC4M7+OFU92vZvN8jI9z774tAN2vVF1T4MnDJos0w4jV/LnP+pXJH3pOffFRzquDG7YCwKvq2tsxAKw== + dependencies: + "@types/d3-shape" "^1.3.2" + "@types/react" "*" + "@visx/group" "3.12.0" + "@visx/scale" "3.12.0" + classnames "^2.3.1" + d3-shape "^1.2.0" + prop-types "^15.5.10" + "@visx/text@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@visx/text/-/text-3.0.0.tgz#9099c3605027b9ab4c54bde97518a648136c3629" @@ -5134,6 +5163,17 @@ prop-types "^15.5.10" react-use-measure "^2.0.4" +"@visx/tooltip@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@visx/tooltip/-/tooltip-3.12.0.tgz#1521c186829bf809182496ab9076fe491aed76b8" + integrity sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A== + dependencies: + "@types/react" "*" + "@visx/bounds" "3.12.0" + classnames "^2.3.1" + prop-types "^15.5.10" + react-use-measure "^2.0.4" + "@visx/vendor@3.12.0": version "3.12.0" resolved "https://registry.yarnpkg.com/@visx/vendor/-/vendor-3.12.0.tgz#36de9d513648b37e1569a963881261dce28b1354" @@ -6602,16 +6642,16 @@ csstype@^3.0.2, csstype@^3.0.6, csstype@^3.0.7, csstype@^3.1.0, csstype@^3.1.1: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== -currency-symbol-map@~5: - version "5.1.0" - resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz#59531fbe977ba95e8d358e90e3c9e9053efb75ad" - integrity sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw== - csstype@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +currency-symbol-map@~5: + version "5.1.0" + resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-5.1.0.tgz#59531fbe977ba95e8d358e90e3c9e9053efb75ad" + integrity sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw== + cwise-compiler@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5"