From 072c2b6904acac38a16d317808f5b7e49838497d Mon Sep 17 00:00:00 2001 From: coder-sett Date: Tue, 8 Apr 2025 14:22:20 +0800 Subject: [PATCH 1/3] Feat: add developer modules Signed-off-by: coder-sett --- .../web/public/images/test/area-stack (1).png | Bin 0 -> 110399 bytes .../web/public/images/test/area-stack (2).png | Bin 0 -> 74283 bytes .../src/modules/developer/DataView/Charts.tsx | 75 +++ .../CodeMergeRatio.tsx | 148 ++++++ .../CodeReviewRatio.tsx | 145 ++++++ .../CommitFrequency.tsx | 116 +++++ .../CommitPRLinkedRatio.tsx | 133 ++++++ .../ContributorCount.tsx | 140 ++++++ .../IsMaintained.tsx | 84 ++++ .../LocFrequency.tsx | 141 ++++++ .../PRIssueLinked.tsx | 122 +++++ .../TotalScore.tsx | 85 ++++ .../CollaborationDevelopmentIndex/index.tsx | 54 +++ .../CommunityActivity/CodeReviewCount.tsx | 81 ++++ .../CommunityActivity/CommentFrequency.tsx | 82 ++++ .../CommunityActivity/CommitFrequency.tsx | 79 ++++ .../CommunityActivity/ContributorCount.tsx | 85 ++++ .../DataView/CommunityActivity/OrgCount.tsx | 82 ++++ .../CommunityActivity/RecentReleasesCount.tsx | 85 ++++ .../DataView/CommunityActivity/TotalScore.tsx | 83 ++++ .../CommunityActivity/UpdatedIssuesCount.tsx | 84 ++++ .../CommunityActivity/UpdatedSince.tsx | 138 ++++++ .../DataView/CommunityActivity/index.tsx | 54 +++ .../BugIssueOpenTime.tsx | 155 +++++++ .../ClosedPrsCount.tsx | 87 ++++ .../CodeReviewCount.tsx | 85 ++++ .../CommentFrequency.tsx | 85 ++++ .../IssueFirstResponse.tsx | 152 ++++++ .../CommunityServiceSupport/PrOpenTime.tsx | 156 +++++++ .../CommunityServiceSupport/TotalScore.tsx | 86 ++++ .../UpdatedIssuesCount.tsx | 84 ++++ .../CommunityServiceSupport/index.tsx | 54 +++ .../DataView/MetricDetail/MetricChart.tsx | 92 ++++ .../ContributorContribution.tsx | 431 ++++++++++++++++++ .../ContributorOrganizations.tsx | 143 ++++++ .../ContributionCount/index.tsx | 31 ++ .../ContributorTable/ContributorDropdown.tsx | 53 +++ .../ContributorTable/ContributorName.tsx | 38 ++ .../ContributorTable/DomainPersona.tsx | 142 ++++++ .../ContributorTable/RolePersona.tsx | 25 + .../ContributorTable/index.tsx | 412 +++++++++++++++++ .../MetricContributor/Contributors.tsx | 237 ++++++++++ .../MetricContributor/contribution.ts | 234 ++++++++++ .../MetricDetail/MetricContributor/index.tsx | 207 +++++++++ .../MetricDetail/MetricContributor/utils.tsx | 92 ++++ .../DataView/MetricDetail/MetricDashboard.tsx | 396 ++++++++++++++++ .../MetricIssue/IssueComments.tsx | 50 ++ .../MetricIssue/IssueCompletion.tsx | 54 +++ .../MetricDetail/MetricIssue/IssueTable.tsx | 212 +++++++++ .../MetricDetail/MetricIssue/index.tsx | 134 ++++++ .../MetricDetail/MetricIssue/issue.ts | 24 + .../DataView/MetricDetail/MetricPr/PR.ts | 20 + .../MetricDetail/MetricPr/PrComments.tsx | 49 ++ .../MetricDetail/MetricPr/PrCompletion.tsx | 48 ++ .../MetricDetail/MetricPr/PrTable.tsx | 218 +++++++++ .../DataView/MetricDetail/MetricPr/index.tsx | 131 ++++++ .../DataView/MetricDetail/PieDropDownMenu.tsx | 121 +++++ .../developer/DataView/MetricDetail/index.tsx | 122 +++++ .../MetricDetail/metricChartOption.ts | 62 +++ .../DataView/MetricDetail/tableDownload.ts | 54 +++ .../MetricDetail/useVerifyDateRange.ts | 51 +++ .../OrganizationsActivity/CommitFrequency.tsx | 81 ++++ .../ContributionLast.tsx | 82 ++++ .../ContributorCount.tsx | 83 ++++ .../OrganizationsActivity/OrgCount.tsx | 79 ++++ .../OrganizationsActivity/TotalScore.tsx | 83 ++++ .../DataView/OrganizationsActivity/index.tsx | 47 ++ .../OverviewSummary/CommunityDropDownMenu.tsx | 82 ++++ .../OverviewSummary/CommunityRepos.tsx | 125 +++++ .../DataView/OverviewSummary/LineChart.tsx | 138 ++++++ .../DataView/OverviewSummary/index.tsx | 79 ++++ .../DataView/Status/ErrorAnalysis.tsx | 21 + .../DataView/Status/LoadingAnalysis.tsx | 23 + .../DataView/Status/NotFoundAnalysis.tsx | 20 + .../DataView/Status/UnderAnalysis.tsx | 32 ++ .../developer/DataView/Status/index.tsx | 4 + .../src/modules/developer/DataView/index.tsx | 31 ++ .../modules/developer/components/Badge.tsx | 276 +++++++++++ .../developer/components/CardDropDownMenu.tsx | 206 +++++++++ .../developer/components/ChartWithData.tsx | 128 ++++++ .../components/CompareBar/AddInput.tsx | 173 +++++++ .../components/CompareBar/ColorSwitcher.tsx | 142 ++++++ .../components/CompareBar/CompareItem.tsx | 81 ++++ .../components/CompareBar/SearchDropdown.tsx | 63 +++ .../developer/components/CompareBar/index.tsx | 54 +++ .../developer/components/ConnectLine.tsx | 9 + .../components/Container/AnalyzeContainer.tsx | 21 + .../Container/ChartOptionContainer.tsx | 71 +++ .../Container/LegacyLabelRedirect.tsx | 72 +++ .../developer/components/DefaultCardHead.tsx | 45 ++ .../components/DistributionMap/EChartGlOpt.ts | 415 +++++++++++++++++ .../components/DistributionMap/index.tsx | 131 ++++++ .../components/DownCardLoadImage.tsx | 287 ++++++++++++ .../developer/components/DownloadAndShare.tsx | 327 +++++++++++++ .../components/HeaderWithFitlerBar.tsx | 28 ++ .../MetricDetail/CommunityFilter.tsx | 93 ++++ .../MetricDetail/DetailHeaderFilter.tsx | 91 ++++ .../components/NavBar/ChartDisplaySetting.tsx | 107 +++++ .../NavBar/ContributorDateTagPanel.tsx | 217 +++++++++ .../components/NavBar/DatePicker.tsx | 38 ++ .../components/NavBar/DateRangePicker.tsx | 161 +++++++ .../components/NavBar/LabelItems.tsx | 71 +++ .../components/NavBar/MerticDatePicker.tsx | 70 +++ .../components/NavBar/NavDatePicker.tsx | 159 +++++++ .../components/NavBar/NavbarSetting.tsx | 63 +++ .../components/NavBar/RepoFilter.tsx | 46 ++ .../components/NavBar/SubscribeButton.tsx | 91 ++++ .../developer/components/NavBar/index.tsx | 24 + .../components/NavBar/useI18RangeTag.tsx | 20 + .../components/NavBar/useSwitchRange.ts | 22 + .../developer/components/PageInfoInit.tsx | 39 ++ .../developer/components/ProviderIcon.tsx | 18 + .../developer/components/ScoreConversion.tsx | 63 +++ .../developer/components/SectionTitle.tsx | 30 ++ .../Collaboration/TopicNicheCreation.tsx | 30 ++ .../Collaboration/TopicProductivity.tsx | 65 +++ .../SideBar/Collaboration/TopicRobustness.tsx | 51 +++ .../SideBar/Collaboration/index.tsx | 19 + .../Contributor/TopicNicheCreation.tsx | 34 ++ .../SideBar/Contributor/TopicProductivity.tsx | 107 +++++ .../SideBar/Contributor/TopicRobustness.tsx | 40 ++ .../components/SideBar/Contributor/index.tsx | 21 + .../components/SideBar/Menu/MenuItem.tsx | 84 ++++ .../components/SideBar/Menu/MenuLoading.tsx | 47 ++ .../components/SideBar/Menu/MenuSubItem.tsx | 21 + .../components/SideBar/Menu/MenuTopicItem.tsx | 33 ++ .../components/SideBar/TopicNicheCreation.tsx | 62 +++ .../components/SideBar/TopicOverview.tsx | 19 + .../components/SideBar/TopicProductivity.tsx | 96 ++++ .../components/SideBar/TopicRobustness.tsx | 73 +++ .../developer/components/SideBar/TopicTab.tsx | 45 ++ .../SideBar/assets/NicheCreation.svg | 12 + .../SideBar/assets/Productivity.svg | 12 + .../components/SideBar/assets/Robustness.svg | 12 + .../developer/components/SideBar/config.ts | 419 +++++++++++++++++ .../developer/components/SideBar/index.tsx | 81 ++++ .../components/SideBar/useActiveMenuId.ts | 56 +++ .../developer/components/TableList.tsx | 277 +++++++++++ .../developer/components/TopicNavbar.tsx | 46 ++ .../developer/components/TopicTitle.tsx | 26 ++ .../modules/developer/components/urlTool.ts | 13 + apps/web/src/modules/developer/constant.ts | 52 +++ .../developer/context/ChartsDataProvider.tsx | 203 +++++++++ .../developer/context/SideBarContext.tsx | 29 ++ .../developer/context/StatusContext.tsx | 42 ++ .../src/modules/developer/context/index.ts | 1 + .../modules/developer/hooks/dateRange.test.ts | 14 + .../developer/hooks/useCompareItems.ts | 20 + .../developer/hooks/useDatePickerFormat.ts | 9 + .../developer/hooks/useExtractShortIds.ts | 17 + .../developer/hooks/useExtractUrlLabels.tsx | 44 ++ .../developer/hooks/useGetLineOption.tsx | 104 +++++ .../developer/hooks/useGetRatioLineOption.tsx | 106 +++++ .../developer/hooks/useHandleQueryParams.ts | 29 ++ .../developer/hooks/useIsCurrentUser.tsx | 15 + .../modules/developer/hooks/useLabelStatus.ts | 65 +++ .../src/modules/developer/hooks/useLevel.ts | 15 + .../developer/hooks/useMetricQueryData.ts | 11 + .../developer/hooks/useQueryDateRange.ts | 65 +++ .../developer/hooks/useQueryMetricType.ts | 24 + .../developer/hooks/useSwitchMetricType.ts | 22 + .../developer/hooks/useTopicNavbarScroll.ts | 103 +++++ .../hooks/useVerifyDetailRangeQuery.ts | 26 ++ apps/web/src/modules/developer/index.tsx | 27 ++ .../developer/options/ChartDataProvider.tsx | 110 +++++ .../developer/options/ChartOptionProvider.tsx | 36 ++ .../options/builder/getCompareStyleBuilder.ts | 13 + .../options/builder/getIndicatorsBuilder.ts | 23 + .../options/builder/getLineBuilder.ts | 81 ++++ .../options/builder/getRatioLineBuilder.ts | 90 ++++ .../src/modules/developer/options/index.ts | 10 + .../developer/options/useCardManual.tsx | 35 ++ .../developer/options/useOptionBuilderFns.ts | 20 + .../src/modules/developer/store/chartTheme.ts | 31 ++ .../developer/store/chartUserSetting.ts | 46 ++ apps/web/src/modules/developer/store/index.ts | 2 + apps/web/src/modules/developer/type.ts | 56 +++ apps/web/src/pages/developer/[slugs].tsx | 18 + 178 files changed, 15300 insertions(+) create mode 100644 apps/web/public/images/test/area-stack (1).png create mode 100644 apps/web/public/images/test/area-stack (2).png create mode 100644 apps/web/src/modules/developer/DataView/Charts.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeMergeRatio.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeReviewRatio.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitPRLinkedRatio.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/ContributorCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/IsMaintained.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/LocFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/PRIssueLinked.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/TotalScore.tsx create mode 100644 apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/CodeReviewCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/CommentFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/CommitFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/ContributorCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/OrgCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/RecentReleasesCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/TotalScore.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedIssuesCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedSince.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityActivity/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/BugIssueOpenTime.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/ClosedPrsCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/CodeReviewCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/CommentFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/IssueFirstResponse.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/PrOpenTime.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScore.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/UpdatedIssuesCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/CommunityServiceSupport/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricChart.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorContribution.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorOrganizations.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorDropdown.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/metricChartOption.ts create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/tableDownload.ts create mode 100644 apps/web/src/modules/developer/DataView/MetricDetail/useVerifyDateRange.ts create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/CommitFrequency.tsx create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributionLast.tsx create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributorCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/OrgCount.tsx create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/TotalScore.tsx create mode 100644 apps/web/src/modules/developer/DataView/OrganizationsActivity/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/OverviewSummary/CommunityDropDownMenu.tsx create mode 100644 apps/web/src/modules/developer/DataView/OverviewSummary/CommunityRepos.tsx create mode 100644 apps/web/src/modules/developer/DataView/OverviewSummary/LineChart.tsx create mode 100644 apps/web/src/modules/developer/DataView/OverviewSummary/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/Status/ErrorAnalysis.tsx create mode 100644 apps/web/src/modules/developer/DataView/Status/LoadingAnalysis.tsx create mode 100644 apps/web/src/modules/developer/DataView/Status/NotFoundAnalysis.tsx create mode 100644 apps/web/src/modules/developer/DataView/Status/UnderAnalysis.tsx create mode 100644 apps/web/src/modules/developer/DataView/Status/index.tsx create mode 100644 apps/web/src/modules/developer/DataView/index.tsx create mode 100644 apps/web/src/modules/developer/components/Badge.tsx create mode 100644 apps/web/src/modules/developer/components/CardDropDownMenu.tsx create mode 100644 apps/web/src/modules/developer/components/ChartWithData.tsx create mode 100644 apps/web/src/modules/developer/components/CompareBar/AddInput.tsx create mode 100644 apps/web/src/modules/developer/components/CompareBar/ColorSwitcher.tsx create mode 100644 apps/web/src/modules/developer/components/CompareBar/CompareItem.tsx create mode 100644 apps/web/src/modules/developer/components/CompareBar/SearchDropdown.tsx create mode 100644 apps/web/src/modules/developer/components/CompareBar/index.tsx create mode 100644 apps/web/src/modules/developer/components/ConnectLine.tsx create mode 100644 apps/web/src/modules/developer/components/Container/AnalyzeContainer.tsx create mode 100644 apps/web/src/modules/developer/components/Container/ChartOptionContainer.tsx create mode 100644 apps/web/src/modules/developer/components/Container/LegacyLabelRedirect.tsx create mode 100644 apps/web/src/modules/developer/components/DefaultCardHead.tsx create mode 100644 apps/web/src/modules/developer/components/DistributionMap/EChartGlOpt.ts create mode 100644 apps/web/src/modules/developer/components/DistributionMap/index.tsx create mode 100644 apps/web/src/modules/developer/components/DownCardLoadImage.tsx create mode 100644 apps/web/src/modules/developer/components/DownloadAndShare.tsx create mode 100644 apps/web/src/modules/developer/components/HeaderWithFitlerBar.tsx create mode 100644 apps/web/src/modules/developer/components/MetricDetail/CommunityFilter.tsx create mode 100644 apps/web/src/modules/developer/components/MetricDetail/DetailHeaderFilter.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/ChartDisplaySetting.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/ContributorDateTagPanel.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/DatePicker.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/DateRangePicker.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/LabelItems.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/MerticDatePicker.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/NavDatePicker.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/NavbarSetting.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/RepoFilter.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/SubscribeButton.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/index.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/useI18RangeTag.tsx create mode 100644 apps/web/src/modules/developer/components/NavBar/useSwitchRange.ts create mode 100644 apps/web/src/modules/developer/components/PageInfoInit.tsx create mode 100644 apps/web/src/modules/developer/components/ProviderIcon.tsx create mode 100644 apps/web/src/modules/developer/components/ScoreConversion.tsx create mode 100644 apps/web/src/modules/developer/components/SectionTitle.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Collaboration/TopicNicheCreation.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Collaboration/TopicProductivity.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Collaboration/TopicRobustness.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Collaboration/index.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Contributor/TopicNicheCreation.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Contributor/TopicProductivity.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Contributor/TopicRobustness.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Contributor/index.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Menu/MenuItem.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Menu/MenuLoading.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Menu/MenuSubItem.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/Menu/MenuTopicItem.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/TopicNicheCreation.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/TopicOverview.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/TopicProductivity.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/TopicRobustness.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/TopicTab.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/assets/NicheCreation.svg create mode 100644 apps/web/src/modules/developer/components/SideBar/assets/Productivity.svg create mode 100644 apps/web/src/modules/developer/components/SideBar/assets/Robustness.svg create mode 100644 apps/web/src/modules/developer/components/SideBar/config.ts create mode 100644 apps/web/src/modules/developer/components/SideBar/index.tsx create mode 100644 apps/web/src/modules/developer/components/SideBar/useActiveMenuId.ts create mode 100644 apps/web/src/modules/developer/components/TableList.tsx create mode 100644 apps/web/src/modules/developer/components/TopicNavbar.tsx create mode 100644 apps/web/src/modules/developer/components/TopicTitle.tsx create mode 100644 apps/web/src/modules/developer/components/urlTool.ts create mode 100644 apps/web/src/modules/developer/constant.ts create mode 100644 apps/web/src/modules/developer/context/ChartsDataProvider.tsx create mode 100644 apps/web/src/modules/developer/context/SideBarContext.tsx create mode 100644 apps/web/src/modules/developer/context/StatusContext.tsx create mode 100644 apps/web/src/modules/developer/context/index.ts create mode 100644 apps/web/src/modules/developer/hooks/dateRange.test.ts create mode 100644 apps/web/src/modules/developer/hooks/useCompareItems.ts create mode 100644 apps/web/src/modules/developer/hooks/useDatePickerFormat.ts create mode 100644 apps/web/src/modules/developer/hooks/useExtractShortIds.ts create mode 100644 apps/web/src/modules/developer/hooks/useExtractUrlLabels.tsx create mode 100644 apps/web/src/modules/developer/hooks/useGetLineOption.tsx create mode 100644 apps/web/src/modules/developer/hooks/useGetRatioLineOption.tsx create mode 100644 apps/web/src/modules/developer/hooks/useHandleQueryParams.ts create mode 100644 apps/web/src/modules/developer/hooks/useIsCurrentUser.tsx create mode 100644 apps/web/src/modules/developer/hooks/useLabelStatus.ts create mode 100644 apps/web/src/modules/developer/hooks/useLevel.ts create mode 100644 apps/web/src/modules/developer/hooks/useMetricQueryData.ts create mode 100644 apps/web/src/modules/developer/hooks/useQueryDateRange.ts create mode 100644 apps/web/src/modules/developer/hooks/useQueryMetricType.ts create mode 100644 apps/web/src/modules/developer/hooks/useSwitchMetricType.ts create mode 100644 apps/web/src/modules/developer/hooks/useTopicNavbarScroll.ts create mode 100644 apps/web/src/modules/developer/hooks/useVerifyDetailRangeQuery.ts create mode 100644 apps/web/src/modules/developer/index.tsx create mode 100644 apps/web/src/modules/developer/options/ChartDataProvider.tsx create mode 100644 apps/web/src/modules/developer/options/ChartOptionProvider.tsx create mode 100644 apps/web/src/modules/developer/options/builder/getCompareStyleBuilder.ts create mode 100644 apps/web/src/modules/developer/options/builder/getIndicatorsBuilder.ts create mode 100644 apps/web/src/modules/developer/options/builder/getLineBuilder.ts create mode 100644 apps/web/src/modules/developer/options/builder/getRatioLineBuilder.ts create mode 100644 apps/web/src/modules/developer/options/index.ts create mode 100644 apps/web/src/modules/developer/options/useCardManual.tsx create mode 100644 apps/web/src/modules/developer/options/useOptionBuilderFns.ts create mode 100644 apps/web/src/modules/developer/store/chartTheme.ts create mode 100644 apps/web/src/modules/developer/store/chartUserSetting.ts create mode 100644 apps/web/src/modules/developer/store/index.ts create mode 100644 apps/web/src/modules/developer/type.ts create mode 100644 apps/web/src/pages/developer/[slugs].tsx diff --git a/apps/web/public/images/test/area-stack (1).png b/apps/web/public/images/test/area-stack (1).png new file mode 100644 index 0000000000000000000000000000000000000000..2419cd624f7b8e697f471d5073c62b18039d1717 GIT binary patch literal 110399 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 qv!=)fHuBWo|2} z0j1Bryvy$F?2L{bMdE>dhkiB12|%e|#LxC<{$)0Dc$Mz4pgl$40*lU5uGvEcx_NPY zCTDso_P{|VsPF|Rg*m*+__9haySv;o&9#aPqL&gaDK7rj`{g~^dMA6KWt@P;Pz;RY z!oT)M5)5QOvw=4_W&Tw#zx1GSQgTwj!c_BCm@vl#U6`DM+;M6~0@Krh!NJpc|0|Sf zH`vuSIM~^1YI1T`Tfi|~iSc9eE2!`@Z8&!e4JfBBtX0Q#F{;MoYkXNNjEg$-;gf&q z@P8@Rk3CeYWTP%zk9)O;UWx~rpDJ7ESYYG+sB7}HD?!M*K&#wx^vz(Nx`cXk5&W&j!vIF-Lgu+zZsup$A9W^=3z96;?9J5;DoiKQ+fWj#J^hF1{u(UIBo7$_*F7DOvITJ zNZ-oR@}-fCj31zraLWZ5GTn@$-Q@s`k5#~-hw63vNv-A_A0N+?acZev^YycdjEPwv z(zc#-+$e7I^qDbT339HMS)GxCvoJ>O!Bt{NR8nma3OR+F;ehk;foQ`VEP)Y+Fo%$g zK?v!A+}y9(y$|L!ppBH$l$P_0^Aluv*EBqZ$0t}-n;npwd`JcCvs^$>Fxo%mV|BIY zRp?`@>eVcecn_y$NycvElgF+FfW!6{5q5V*u?skDj5s`rv*izcCD`tmofI}oFUJcd zqVikB4+(e3%DBjM`Vfx%`bkWD;a`IIUIzrJq)%`9Rge$fH9c$b;{5t5?=vLjtLXs` z6!kdXb7}I$mW9m1>e@v#4hsXx`gh_XBWJI_yEiLUifO6E?AM85Sziy;SbPEg&4bjx1LAy@B{-tYI zPIi);^KiZFaQtXQi?<)wzpEQ9Ku5=PgzygaFGYwHFHA(e9NZ)!A;CmdUEN}=)J&f7 zn&Cm82@~~LSCu5lJYuhUFL7;s-SKEK#$vR@*f1oWh{_|!D?f$*R|BH+Zaf2~cE@Pf zCa(VjC_ePJXAt-`@zEa(3neZ4(qy!X4156OKljae8cu-{M&_%gI22`E1ziS--VT$C z%!sZ;34vl_M~CRgUc~$TqX_h0p@1#SDgWzK082<_TwSWJuCDl`Q>C&#T!>_Ql699f zIzD1AroKCF)OoT_2R`kL{a~C)A;RGa05Z>81IZ=4&Q56Mw4Ot>Qy%^cj)-oK0jMbL z{?e5Cm(9Imo8*s%b*ZwM;^on2! zKS~%7qxNkdBKvf3U8lNn(xiH?CJe4l_%Y|m;~S{g=xyA2szs{`fk4%oK!d9 z^L_=R_hUd5!)-Ff86QLJxxo|+IgE~HOdzD+;wEfkSwzU!S6aoO4V&!I#>U3Wv5brG zD-JnTrF9-|N7T;z1RW3iS$|oFXoMXAm%3;3w|;%IokVWa)*(8>i18Ez7vC@&K=PI3 z=bE3enpxKH3r4rPQN_r>O(Pz*!sYlVQ&}m(P9%b58NI9on4 z;y3BxY~6}T)3}YRA0g??jjP`;sTgfbxGYanFCXT7`IVPrP=+*9-JN(r1*4w3PdFtH zeMDP6m3DX2x`8@E(uF-plT&m@JQ*{?~i~s}3M}Gqspj@UNzbx;L}~uF7oRf^K^#O-~$2gE*A!=Kjn}m<*=Yxq@?S6IIMk1h%PBC}!!S1iEQv(C+ za&8@$=ax=3sw#GGYG^m$_m(6yArX_}SmF_n;^!a{`CG;($K&l+L5Urbh{Bgaw?f1p zT~VP*sLB{&jB%z7)7+S}RX`$Vah7Y&+nWVCXqmcLbtum=9y<*iF@rse5y=5Nkjcg7 zic~JXka=O%73G~)Wt?V+-yBl_O>Vd3c~wn=?MX)3M-hGzUiP<{7avh1Yn5*gG9wyz zRudQ|&tFU&?{n{w1@8QUmvlU6s`_dkru(nmNa9%L<%-^i6Z0l;AsH4VCNmixi9Uii zxeHuuuba#ieEO?)rIOI&2UsA1f5}*h$?dVk z7a=AaBs!74V)NzOteqqyIgy`I`vxm0U>SZZMoVGr1tOno^YLk&u1ZZ+#TONK5}6kr zp>pP3nccHmt&_43AjXI(Mkta0qElsnc*0fr?xpans>riP^e@M(;O1o&)4boh#he$^ z$TR^5aHX47v@?S|TyGgMyB@|XHj!~hliiu{o+1>? z88#4!=N}(_Px&@ur`Bcc9j+PryS!qs4C(MA<(ss~!pL||ZtV&Lw`&9ajlWo6V;eb)LZu><;1mr%tH*WBaw{WB) zG8dY8?PUG7zMk&$;=3=_4iQItT7HDXcIWH`jKDNdG-1B`&r=M_ws; z8ie}e+?*;J-oMbGy}`RCKpa0t?pPLdS?)MJ1Ap*zD=UvNvD^ewRQd$2J}9nz*iSC0 zD8vCHt*^Gw9Mj&JRxy774E(T3*_2A|R|)-s)!58M^S=1ZZnABXRSs!!;@oB}pB>jd zht!4iM3EClV_^BLguUv8N2v&LyXI?lvf%h*tx0F3FpTsaEPkR+x5B$iQy-R#z!kV8?g-F}*nLRfxLVW!HLIV`n48qW>%?mM>yxl_214K@@v!ms* zmq$A+twEF(I`FpmgapN$f`alON+D)zmp&OXv$K<<9*|pgef`;InfIho8)Iect_8KV zIu%oH>jg$3;s^LxvM33O0btG&P(Fn@!+rN(Oyp)W=6G%{jRL2BNp=)z+pcjrLv*kp zG<{P1rH2PEW2fC~a%s}GukX?z_SPUy_F>B$P3h5s<&*7eHpsWUV%@OL`#q1e?iivD zxW_8$ZzNs$8}3HH7o5FbWyFt-jkyAo@_j(e2_vVPWM7?s`{0?%M+tzK3j!32L$6jN z`TS&A%%uCnQ4A1wr3EcVi1LBhYt$C3P~l51>n=j0E-&T@|y{Qer4OnLntu5v$X&;>p9&(U8K%?7<^iY(5Im9H9hML z{7%mH9hih0bNFrMfq|3fID%2k^xvCJN<^4wc3Imy1CYNLBSrM$^DBM+{{BdA&64MP zH`I0dUII*X>h>xv5H&-t7%%rEH$VYO=KVJ%)1|$2tfwxHj2rbLeXwlCh>5cYFtLx* z&vkxjW;JeK-k?dLxlle1HX*jxvVEmy?ImnSC9AY6@3-1vaRP5dRm zF6miS?Cqhsd3jOC?i##KkB)=)_xGz$cDl+eM@x#@lyQRVOP%v)Z(|YN%tcc%$LFSG zzqt1)(YB&jwPo8#u`G)TK^(QQ3eGEt3sOQ_Hn!B{&T)ONBQmR~LP=!N+rs!u2n#0&+%fiS^x_)&iEp1FFfPb!2WFdgImhH ze^LMQA%WeKYkcEcdzBb;Jb^%r@$WN$-}p&>d}cYf=R}sNXo7CqMy9QN9P#k4qUD6H zuAPbQm$_|Cc6F3h4qS8G;v0B|;Itz3B)y-u4T9^0$RQRX|kC zjaE(h#6(3b+D}>e+@;HD{K=*1u>kzMi*;|3?@c&;SVpWAL0xZ zR^y!fvT?ZK?Ae9V1m+(hh=&mi&~Y=3ul44!!9$$jhjO<8qoJ$-%syYEcrLS~q$I!R z?k+44n1SZqOJ(}-Z&n>QDkGg{&yRTlYlj)=cM8ehzW)!KrxWs_2EvpmSU&je#CdC` zP$gjpx`yYL3SjU{@oSNyO^nqY_uA34Z_zd2DGGXW~40 z!;@S1F94%^F9l5bzc9>Fe(_?n7(htS5RXYW6@YxaG-6I0>ETyD<>!mv^5Ehys`yc5 z+pW+&7pxnAdE(p!+>hHQjUYIetH$!=<7A)WqxgRD4`+4SYST6lH&bqOb3NSewXWTy zP1o09{jjd8*?rSq%70Yval@s!gm(km!Ez+w%Y=!%TYB@9G$hkwkh-SgA%r`-Ib}gY z8Zku$Qn-V{*<25`Io&2^wFPd*Q3yr#unZ2=8XY> zlq<~~))0mwbF_}9sC}_;i8#^Wx;A5^o-ESQ*RC@a=w#A0}NxItc znED&OX*{(ZbRGHijt(!SW8X3!t%)ylRn57@^;l9b?xI9vM6}IZB)RtfxiiF|t!kNO z5E#-=;0#`_bO;AohikgrIbCb$;b~+No1yrh0o+%2;kGxc3nX|{tf!2hdYY$ z<-$KkQ&M$$>@sj7a&++kyujGH5^!E!I6k@#-6ju3k z&f8y^vIK7>Itw&^f7bo1Fbp+~BnS}w+8}&uXl<&Sm}+wn+9vZi4{Qv*zMY#}Jf7(s zyB91RtXRw{zxg)KJlDM@gBnsU?lesf64S+7Z?FgI0-hv1s|G@1^!!Z|CW@bOMoVcE zTJnEkxU8n=F&(sTywtGk){(42k00Tr$4u?Y73;$S(|tXY%m5eqVZFW9K4d*2%QloE zJM(sux{z#$+6=<>`Ou8Ys|o!_T;gp207^e;+yIgSXXLb^`3vzWM0K8-sbHkq54!L@ zrt_3y0~0-KdRYZXO&LRx>hO{J`LL%2nvSOzJCxYUfG`yG{2+a*+jx9_B~EgywnYKo z{~%pz{W};OJZ;QS;G(W5+_w=2JiFGrjNfA6`~|#y`fgUZr1)trWo!DwF4>TA^++&{Pv#y7b*pvBcGFBHW;z3)D~z7oX*hnx z^vd1{fgbc-&~JQno03n30W3c-HJXzZ?NkL;eopa3^ghGG2Wu;VgViAO+(N7jpHxqZn=p6##6QR@il&3YL!s=A@TbRvw*S7{4KGJU595RE;p zW&g28IL$ve(?B|+ta%isp~VVC8h>~pX!d?Wf4fML_%bFA$tQ2Hk+EP;1~D&cFJs&a z72~@}aOI~}?y$}vwxUZPz-t(TA{~-5K zOq@c{gq@MQxfIVZ0lBcxiEye$r6EpoNS8w-9O{371y->WcaQNhM~@o`ygUcw9I1p; z{Uc{&WA|>iyGOQrW=)R}^kU8V(Q-}AnJZFft;eYdD6Aaf#E$aEELFPqzY&(OIvK7% zXq)X9bNewn-Pnk0sa?69|1Qm9(lU4=`F%zW^U#2fn2mGBQWH>bBO*ApS6AXMEG{lC zO-s8^d)ynq4-|}ms#l(c2qF{UE3(``CBDXKQw5*{e;3Hh*_Fm7fihi9T3R!@QtDbD z@mdMuv}Lg8<4#a7rF=>DRS%5%Z?pGlz9CoF>tdaI`TPZh(Ak6=sPexuMeE zzt4h60kiO)Lz+hAiXRSHKoR;+(@`wUg98(Z$u&w{XTS!%OwG-!9arI*{mEWut_lf) z3iiw03YTSjfFkM{Q0bL1b95{NN?gO5K(nR<9gKk}_j05=FE39vyVG4)HxW(vfr1$4 zlkCTJUtnC*v>tOw7lA2n{_WO@pF?h}au=Klq4wQiPJNro3~F6&s)A?|Jz+gIHcL8Q zfx#*=CXXj78s!GX8Mxx2lU%D#d}3a7cbf61sIN1tRyn+dRS#!7Szk2_t2*z<+@1>n zw){?dhCePy0BH{#*n}pPZdS~9Sop%VZ71{P-V6A>uu-4lQ6Da9MzgeuHcbuqQ;Kzn zv3otCq&;^8>cRUov%Y~G(V3F20k^F>^V4r37nczQghrb@&~hG&>91oPCy&<7)}|nk z)xr+mRLetW=i?8ce7k8<*}kFh?rwkKcNFsDV-&4uw)I6q?^WM95tSphuLUEMJ}uKV z+^?~B7_ej;LI-rz)sfv-Ct04oGS77b0K*~zb>)tn6@lbhy$k|CS(#gS+$#m_! z$e(@U)CIIGw-&iK_28E6quau6DHc^#i9p*UJKHxs2G|Xam6er5Rdu!7#HdL?53W3N z(peuTAt~I!Lt}_*{x}(w%L8RW78vUYL}FjsuJB}aPALBe|M!?ewS27xPm$9N27ntd z0cG>t=I~2-YWa9geP0cC2Y|%NS#qye+Az+yTuBjF*UbFL*F+HL{k#B5;OT0Gdig+| zIm4QdjxYFoT|rb{zMd7Tn59SZWk|ec#Vke-5=krtESLBI)1=C(%5d65rW&MlSR;Cp*L~II85- zgz!QUg(^8PdFa7%A^rxAkIG@F#&9Q&SM**a?CgV%p_JQD+R7-d{t=#g>;dLF!a8Oz z3edRaZDBljs?+0&hSL{=eq?$YPG1FvUuUkV>gyL4O)DRwfX-voNrW8Z84)wk59Oaf zt}YVwi#zq?ZVdzbR{~{aB3iompJio&aw$>aH8)~GefXR0=Gp4?f-ZvKDK%|vcP-Q5 zlcU|1_hpskb? z$!~)4nZ?r)oy?&w8n#X@1BUio7zOM#F9uH z_6~2P#^{W(A+y>oE9P7(nuigK^IFIpYsB>%GG6nD>kBkz9(9@P7q#elZWgs@i)^cQpD6vp3`PDbJDoz0X#u~R=37UGzwduHTmQjc~ zeJZG4yPfEcs6zKa_uJ4bTQ@(-bhyY>yWNcLN6q$I;_~6In{MrYdnWS*q?`PXfG_Z| z;#0UKoOVLoa-j?D%zMl{(vY^!Qz771yD6-{8idmK43?YyBIbx0Bvx!s^n~0CL6=XS z)=W6JTLw%x`!NC4&)}fTiY#5C)(sF<{?G^n2zmgWfb~XqwGl`?0%YH zh+;g--j0yIAIh}3*+a9vUl<^+6EN5(YeNDd5RY>^MJj{ZfFShvq?xQf#iFpT+#c9? zAJ7H11W-&2um^;dx%u%nJQJLqbs&5Uw2F$_`ik5h9XR;Un7RbB*ZoWYcW%uf*oCa8 zP87+Tln)<_1p-+-HqcTrf}?Jt+wq2k8ACZwvd?7T#H(Z$z%)ee$B(_?OyFi3S$)D} zrSXI~S$du|Eqie+#VwHIr;ds9naO0T?>#}iCyg~G)Im?ElEuwVkF6Vjw5fph7w`}O z*AL(=`_+pOClv@~t-F5@KBSyMAblnCw6E3Bt_M!UT*cp7-IBrMZuSY*TIqf`xZdROg1y}?;50mgsA7}JeBRh~U>u5nb!RyCP4hUV|y*1Gb*1R!50N<{Y-DUmQ=={R){B^@_aHC z4tYO6Ed$xV?BFC}RWVHi@xL;f9x}6%t)QR)2I8csF}kT;uJ6z%V+h(Q+L$F|q}J2d z_j)$6YYNPs;L+aHY4G$I={oQk2*9-Wd?EDQ&{U%ks3zUL`FH+;f$4#_ca7?Fc_?4| zydH=nol)v~qj!M3eyk|z7~nqaF`U|hc8i@+<5%x)H`XvtaB>VKe!z zFUpE$?pB{|q7Z@b@d)}b_wvTyFmq>@MO8gvxG3f9&*r<+RHu0!vm<{;=FwR-->7~B3fY*#^dH_4opwKfBE{` z2P1EUV8RsC5Oi&0XE9&9A~;^c!|m|(R2RT}jo*`UzALC&qDTgOD@3KKFT!K?2X$x< zGpu^0nAus{ei{%dnN~gRg&u}hZTIXhWL@M7p-;iQdQ#a$1dnuO{S_81_X%b!aT{MYa>P> zU%pvC{PHa;^%`qtQa=$v9Y7>ox7x%dH0hbe}{swMcz^@z@EGPeEK zWtpf$b45QbpPmjNO>SFCjC;IRd@6BkH4j1>UF1GEi%%LjrKhE}iBNy7xLP$}h@j_j zd&ZprGe)151@3Dho5#2)xmbsh499h+=D(0_3UA}%$Jjoz18hDqq4Z#r$C3Ndb zQ(q|UEs8hanGoZ8>)-&8jy;nMPX9!nx~x&72zgTi`}FREF^*2~ca$?M#}zpBoJXrR)6 zvYVIYmp=dM7Ld@e!X~hOmaynJi|R^tNQAW6&D7RRybgptCtzAi=$Lcq9-YWK>xLtL zP8KuP%-=@NxIM=r>FV7YtdJ3-?u6_fIFnH<@=|{MnxylV0i2aEPc$}&&?h{Bn&?1v zZIjpeJSwv}gV^?@YiECb{lY^!eZX0d!ZUtJ(1%GzY!OwmUl<;KTG46~<1i)*-dsBK z;wn}p>A4DMv%K+bheFrnc@FzsynBK`+K$#zAzM*hu6Z$79Tab>;=XZK3R2|NPP%MD zgeCu6uxXM-1K9la=h1o;FMBq1-9Ye`FckMAH=C38Fv9V%GH$~AsLC%qz(d<8{9nn( z{Pd($#+l_L?fSZmh1+o+f~8%V(Q>)Y$Yp;OF*D%KX_BZ$lDI^YGco?EEao~Li1LMA zO|)b9Okg>ws(CUB)+oc*Rn+fh3ZEsyMD6l?RDcnbxA`8;%v1f@s9(O_Vn~CH%nJz1( z;n6^l3d5`AeCehRNltPYe{rsoamj%aeVi$TKGe&JLfQSNY7vhKWMHS*F@j)o%qu>r ztOf|9yYHC&G;Qq&&s*bqp^1q4FBvG2+ZK_+89x7JAaM{fgZRki(hge^`Yl9fb{Fc zU!TJIP-~@o-0)Po`YZqU3Z%gdw%#$!gt;PwqHF?2`nskH)x`{ioN}KAF=r{dLQJL( zq7Dv@LgBuem@3LB*UQf59*p8EkmUBeWYKl(Y;187v+Vk#NyDVdnoa0?d|*m_N`L2% z1}pfM`jy-5!ta{#H$$?tP-$~6P}oSD=?Wn&=Yy1+*pKnuu_{s8Ea#F32yBV&To~=) z7P5OGsl2LeK*h?gPmP#6g|7D&X4>}XV8a|2*$Xqiel4<${Irzb%KKrb{iKFu6>&pM zX_Ge``l1}qr_|ows`ho!)KUu7#(&ZO9KMtU`>>qMmpGuL70(>;oU0*5QPNCo%e{N| zWe=FoHCbf&^Roul2N;VS7jWgIFujQ7dZP8OzBz8xFN<7m6f&ovJQpOxR13To3S1=A z)n|waywqIfMSpbQuRXT$=C#m9b}SAGPHQ2QZx}nA``EbZ3QoMBt|HN9ea81Kq%+2di zTJkF^ZsWN@pY6z%HB7Y5BbcO;FlSgV)+>#VxXeHIOZ8+z|JdLe_SpKmWF%w513OZm z3w+~g#;eRu1OwV?xV5+oeK~Jn1dTBk&x%7#p1#I|+{acGj#sjBWd5#wv;=ngjol*I zyq<7S&YaSIOg$D^h>^4Ot8*hlnCAx+`R$9iaw&~ixl&2S5HWM3;yCQ+8h8|-dL!}%WmXY^cXTZk_nE)+cN3hCr_0iz}4&VLifOfiO(7f%_%7r zdxq~u#Q>)2bN_~^Zpi|Ha7Ea{hduXUPEY;r`};~$*M!l_R15iIsS3mEejMfZ1-ElC z^8q%$knd?+y+=6a#W>^Dm`!7QA!J=;x>E1>)2f1 z&jf{~#oh4oc2}1*b`}4=1nCIw3?ctIaRe6Qw)};Qh@7A=h^Qc}z5Y7H&m#l`yok4< zSF-Rq!=Cb9QleE8x66@Gb)a9jZIcTd!LdL=8OIP7z$Uh z4sys4B}NG4w#q$vR!v;0EN=)EHM0gyazV2)4Pk*w#7|#Mzwt&kJ&(XtPG2%~is$gC zuRMDl?KsbCFnr9G@E0MR0>vYNmLclje)xqCHk{V;ozI7@9v-L(Na6zYH4Kncovb&I zVUk`N^ETD09@>ZhQrieiua>@-sP9Z3*5gO#i-NI7ObN$d08Po!O=Ab%4Ul5$AXF>V zKi@h^D%?Q}WdQNIGDD3qhjA9QFyv7DgWptd!&Bi+zo{BrDQ93i) zBRLs&&C%s6a$hL~piY=x7)4@~b597awPRnn5o~7|$;|xlMlf@$X5DuaL(AmziPZG{ z!|bMwbKk%W&)!RJI44tHI}&*p)osuR%*!TKPui%1ajw}v;$q93pr(y}w*=G(J85w2 zx^RfutBRp`;jpIZS)b7>+MCrhjm8*R*+y2_+gC80F*Gm?=)H$C^ud^icf1wDPEMaM zj+8&|xB)pJ#L|;a>4_5uNkJD`ETjgkyYW8SL_(!wzBN7-?i9|Ui2SLrDyH80=N&Z> z5Pb%oe%Z&+>??;180U0S0ch!ciRqM{EgVVAK3j&US&bB>8-%N;0p@)VsK8rd=->Rn zw&(S->0@f@h4BjOwigwX2M)%@i)|NbuQZljxpJi+7>M_Y>;MK=-vLt%4yvlEC!Gyb z0|Tla%ZbiedNqf0@B4atdoLSY^j<$mj^m&D_>Bqt*cP&09@HY&sF*vboLZL!Z3>jwunk6xV&Lup*ZRYGe90C(+4>X*O`ueJL@0T} z1LTlVy2ir@e92&h-6B9bY^b#R`3& zO_z+T^;<~WLuqh=g^0J;7`6bA>_pMi*!te43W}9yk^f6%I-e8bH{5zBq-1y=MyA{h zeD2IxmbTtq$sAE{(OOr=358lM`%RIqP2njCyzU+-?pMntB5aO)&+FAB^}= zTGtlC&OH$mRr=VyHLkl$D{F&!E$c)1%jH-a6w|F#o4A57wry5}8lahK~6F^2MFLFMobuY1gRucge; z2XiybJ1m<>xD#fT8}!BMW}l2G2(9(2xEV;%I zjZx?++`+Slu4XCT;$30GnVY`wjyuIZaNo$0QJ#7smtKXHZrk1}Ftci>kVQUrE37L> zM}YqcO`~A^i#sVo{T?~uxFpp8`1^TYK4J3uzXT?FBYR7o+*S7WQ|N{820rK}c4)N< z+I4v-d_+UDFS^uc6@QEE<4>{pj-SxRdw8}k23?njH0q(OQRNS@rWM;yko7BQB#!BA3;#6U0* zE$jenbC4y#{b-Zne(#6R0e-bmECtjJ`B3lPH$0q~lu+OsC&p+HO5qOaS2IVSdMn3_ z_bz71K)dr$f&Te+ET!tj@6aYUL+6*iwJ}k(n+|kOK7xC0@1SlRY088F_hXPv+I4Xv zmJ+P5a@B0`!hlW*J%&3lV>cZ!HNW!Z-Me>-j9b*;c&82~V}N^o!HB--@?!0jy_Xn6 z@eo;TWTrg~9Ap5g((Fp_Lq9uzK&ia&&L@9~?Bjdgw6O3zlwtVQC-Mm?Ve-&(i!tHK z8N@{ z#^CrlFc3~+JyzOrE)g~O?go1(7vm{rW1HNWC;uwqXhqiIvOHMwo&mY{m;>b`59+!% z*~9*GZf^4Z@5Gp>*N`25G38dN86H%+7j@I2rgoyX$)s`{*J z?A+X5RE@X*7C)f{uz0{iUDxpj8Z0OqB|~*QDm&W9xFuSBXS5g!8I_`|r#@X=c)~O8 zRJpnO5}<^ZnDMs!f(eGaHp=aT-x4+7HnYO$?)6c+VAs7L!MS`sHPB z)P)lU@2*QdbgJ$+$h=S`-1>V zV=jzhWci%r#qX~-u;2W#{WxzEPGpffASfX!nnKUtvmNU0rP?35=cmiBrQA(M~@K!HPklqZefM@3-*6k3x?l^A66atTrzky@^|Wl3?U8xQJHq0U1w8U}Mddm7KS2 z7OvAC;@^2Foeyy2fTiI3&fxtYw%$4{%B_1JpBWe$>5`NdP*6a+8&p8L5n(`DQVunP z0wPEwAxMgpfFNB1N+_*#gCHH!&2JAL-}n3euFtvr>u|yIJbSOb*1hg^ueDXo)n|nB z5AtNm5&SWJawpC!kukE)cy?{t&JjNp74uvA6}byZ{tF?Yodcp%aS!2A`@FwDdnEEcE|>A9k5&uHj-w3bBwSNg>dYM8YKqb{2-N&;Pk2 z0Ic2&3k*9_@Ip(sb8wjnxC{bC^vxP}%Je(y#B!t_dct?48%nr8IU3oVpwXeP>S5@* z{U=Gp$O)VopQQEcq@3@rqv|R8BC_l0ziV^lwDjjKdu0B{a*{+*Plb+e;-0GflXf_X z*yl!VgoVojvsMXyQd`7h9nW9_4UbrZVPo8QxI zj=l9p4PP-06L`5DQQkpC*S1hAZEY?zcY@0j0cJ4DxN+5=0R3C;9hSS6*AY|C5{eE*!bx!u+xcLy7c8J zHBx|}b1aIB%PT)V`NsY{SG>L=7xpXD6uoB9pq7s>`s8h{Y7dC;J13_4+1~5Jh zI`|5kr-RrpPn;BrI3D(}t7LCr9?DROG;>oksxMfX%(o&tl7^^|2wNe{u#HdOrYPs` zi;Q`T9yGC}ORZh;L;*@f2f(O>0oPX!r2xJ1mR$YslS8@sDxdzQzkzoiKJVCs?`yfw zd$GL#*+I$n(gUNb@uJ)1I|pA8ez*!U{5DONMvvu>zb0A*e3?5Au55;(@7oS3yK!~x z=a>r;$}&AdRItQhd*$qp<}k5EPIKqm;9G|x!Q|q|CmkW&yZixnB7&^`Mn+i}#5j?! zhO}o=!%h7kvFHnEA3ww8@#?sWvWliT3<|g>DgEqV^8LNpa)ce_3$PrfEKIK&JCO&u z;Yuze2PsB0$TaLRhSJVn=fqU7ny70Sgv0;;M>y6fSqGti@KL`0+&J}=w|%>yHwoo+ z@cyjuT{QBC+7KMUT=25Bk>m0OMp;U^u71#f(TL5k!e}5#m78(`rBaanBqKuP(>q@Z zX~C1$D79v}CE9InXFUMUd|>eX-_0bar^c(PD;0X)dl_jM9?1GKFvjLt z-q)JVvkWl~!yGis5=OrQY|BzRWMbhF+m?#p$)h`M`AL6bQ@Si!$HAfhlKtFo;~NI= zB@`~Bfk|bN+W{Kw8#}KAnbNTQ)!5iQ$MO?3dcO&KQn57$FkYxra_~`2ZSH)dd9Fg# zQf9pW2^noH;G;@zS(?>`bqk#A(5CzwB?aVQ3Y~*=TeD+rZ6}<0CZ24-#blu0DutVi zkP;lp_bajYGTygD^y12fVtJ7+-IQf|2SA#&p=@wA(X(H!?!Yp)X0Chub*pX$ASXHV zg^3&z;hpw$nRiS&Qr2L0Hi$y>>inCowKo+XM>yb#4dkzZJ>COnZ%^alTjCi$tR7A= z-#kIVRrXP3ORjEXhG>Q=*@05PYeZv`i1|`xfye)g$?>U2O6+!%lAYenkPf6>ZePSb zSzzqlng+o7AESm;=hLuzaF)ZBHnFwNZZnjdv6#$G+>fq8+5sH_$*u6O$6wyX3njM9w<*hS zMoSPc;ddDm_c1Ba)jb=G29AyiWbMw9y6)xo03GE%L0R$Fy@;{))mnd@?pvGotHwSb z?t0rD(?^wi`BMgY$1}gJPmI_a3p4Mt=!V zOcw#HZ~x?EgCAf<781?8=s+!fF;Q0#q69fz6AwIV%&w`SdvnLB1#k?;JUl$oB95;; zi=!jBd!G8AiLvy$&5sI5Je%)iAR3`aYVGDT?0`G0K|JqFovBWxgHc z+VfE;*1t}lMH3DXHEzV|f$l#DO~_LoO&vXBj6WmLK@z*(wpFdTSs5SR@TIhqKs9NW7t8i5V3=JjP9@(T-B znXSOXj-WI?H$WbQIBlS%kyrz4WA-# zdkxh9JWuF;*_+cmN)+~$YnF)48QU)C0vJ_r-UnEZ@E@3jedZJ-p%k`VNhr|SM8e3( zm?Y)9-}k`FJGQ>LYYKa&7*xFlf515{bdQO?rkYlM{1>E(2}q3h^1!y*3`kQvk&gFh zUY>q;576ZL!_6oIk6KUn?G{&7OMepn;D5jc`O_L%6aTX}e0WT(T4IN4Q?{1-)e)CY zgGC13<;eMLW}w*!Unc*tqojyTxvu^ZpC!TpqsaOFFgW`Jw2s|0tZ`fDm)@*!7|yD% zpXilTza=CjbV%~q4~G5<=9Z+JRM_iz{!`g6@}vBf*RP<&z@`uuQ9-G~j!kWk0p7Vg z4!EOj4JQ;8P}+{*3Ce7YJMRZ9rjT*XY2E$c>{EIjn?ErUBn<2XpOmsy-i(&0^$5%; zrJNziKWh*FiPX1Xn@}U&wTZ58#Pp}J=Hl$pg0K>OQT0uiT}k8+u#i-@oo*3az$`hW zOZy*xfZTTgc5;z>?@bWkEZ2(i@(zW*)|CtY2DrT^BmLfdV!EINq4i$CM^0Qqt0yvV zI=bebDG69`ZwI~yAu}52|7cw`o%p<5n%RpKNq90^Vw|u1$H7HkLrF28**Rr>+BJYg zhK}BZG<9kHuvfnHB4vi?4jnuO;p*wHFF|DVG-4=_j{{|)D_Wx4t?4^guU?&U-Kbf# z0z*lfAX`nqJ{sNjRtA-p7uN28J6Re};?hs&)t2XCWfmW?2c{E$0g^JCakQ-c9Wqoo z@l)Kd-xFIALz}h~N37cR%VKDnaF5H-Vh3_Qo~7A9qwW9Y(&-7 zSmhR?{_4V5W%lxz@4SyUfRbcHyMRjC^lJ(-URC@YICxWIteN%h?rxe7W_7PI(xsun zK@Px0zC7aqo=x-Q9i54>@y(jq5c=Fnk01B2fco+tL_c~hiD(3qg}o&T{>Ald#_^a= z32j0`sv^=g77>B#oA`D$HNx-`E}zj?P}1_f)d_ z!hnwE-U%Zy7;+K#xvAG@s~K13xa8uvWENuzph4g8-ZnEQ&!SXJhnm_9pUT?=(z+6B}bVAXLr3RQ2J{G1a|>iX-GtK2&~fk zxgNTw>`~I9E^A>dP|GW-O}fjN0>~aLMz$F5u!Rh115t;3!ZL)lwHeuAn7)}}# zA%*BU?%MiQj13d2u3`g&%vXZT7c# zxw%Z-3%QxZw4L&>)7u_@m=~7Mn7s61AA>V+9Sls8!GAas;5z2<0abLgpQt%0jZ^Lv zNAv0*fDwVODI(p{7=*fat!<1Mu6-+dp(dxwq5+=^r(u31=NiKg3T`#*ydmuVj3#vqWpq6gexuM7ARpeaVl z-^~5_B{eE;93NCvM(z9eu?G{`p_6gq3@egvu-9TWzRl_BEheRFfAYq!jSFeF1zDTd z_oCJgeaKhGmlhu{Pcb$?cmxmQRbFQksIa+zH~RZXRThrE^N&$_b=KP=kjQtVSFi$Z zT>Z1!=x7M)M}#B{!CV;DdTw-^oZA{D6;9o7lkQ5>@R2;3>y?gJe~&1isVG1Mr?<4Q|2}O#TGmA9m=IzM8Nn(K-F-a5a8)vx2ZMZJ z*s483GrEw#yvL4yKd?RWZ?Z=V!}epMbU!7&Zj_KJT=Hj$qGL);*PBm#yA#X`xnV?6 z4Fl8%*o=6P1urYpf<#00An4K@F`rDE2{N=}Dp)}8OKI_8N9$=k{=vtWF9IejEuJJ( z-Q}D%Zg}$ZQE)Y|CLCG1u?x;z;iU=iIO ze`*uar;2PFHu&(DQjF$;{kGe^iZyi9ow96uHKuNdUHIJ}OQxe{5wms8PyG3i8mwX# zowIqjnx*`Is;|N`RwSmYAdi+2b$xvP-DYPF`6f4f8!6 z5x`?4Kj{k92UZiRt^1$Keng?Hum9Q6O}u~^iCg6s6eG8a84I5LITMeeFe>Fn-aY^9 zLI>mZ;ig8e_Y@Dd;MYOs1T-j^(z}NuY#KgVd>3_SG;d?^CiT#bT^H8W%S9 zlbZzRmcn&V9=xV14Dv9{i$u!?{HX&!cW>e_x-x^GB$be!%)|TVi8)BDDYEDv_@)yLn^Uc`ldb{E$vLeAJ%F<2lADTNR$}; z1Bf5E=B>#IYG*Jk>zh~g*oeg3sS8M+;io13;Ov}q)92}sB_2@*46U!40Ri-B9t6kd z`E^jplIKqQD_D@71pU};?a57K$4IRHHDTpL634X zmAkr76LRH=-!=LgdY~%UvK?K5!{tn|xqI-d{ zdL%%JaoAxqwusyX!)+gQ0wzwPep3J+BUgL$2t@64=BDC-GHi3R3?OAp_HS)XJh38E zYl@g_e1^Uv=zASv?j~z5C4LDM*|nYmQvpNMPA%#FHz^{bu!~ZZUg~hky;ymeR6(Hn z=N$y>q1%)Y6*^a6r$;}xwX3oTIRw-(tYd&}W&FHvh{!`k9i$#_hQl7Q$L;BfSuj8( z0Izv^wfCbnVAu?IJwE(hMIR1K`a&n9@nKwZ#v8FOZ_bfteF;V_Z)4{oQ(a^=xhciV zRyr;APkt>&qpJyW5HwU&{zUUuFqJE-U4bmWIFg)x+^+vAj%{y>vG4l?fT6vAWxN{u zLrZ^kX@T}%BXqm>FI)8pQed|q57nGEzVg!G@|((Z&>dA2Nw*HMo+@}g`ngyxsIHVi z0&|M#@P+chj}oOSx{g()H&cDzT5C$r=QI3yK|fI3xyn3j>Zli-ZOS0r`p3+`D=5SN z6=*QSePG>Qv|FnYUKwqdzm(Ajx0h|CkgXSIl8FUWR_H-+fQq%ig;c+t8SDiN}fCRA)VG6kuJkf|f*#Tfu5$DdWn7ff_rw201(X6_pJNETdAxhA zbB&9i3%f4B60gR6CfB~FkEtx7`CM4KOZW4lBHDQnc4Rq zCtSfGi0`-2(E-2EcuIMW0HTUA%!v`m$5guVPmKm^2vm%O83_IWa$u8TCs@F&#dUrw@;#n&?b7l-?+> zZeZTal^ZHfogSSOX)gj_&+IvW|I(}FNR34NiV7N8uBuOVs0a*^G1UD-&kY7lcwx8|`#$no#u`@IPSE2!p3@3zH@&?43$y63JbV(jo^mP$7M zlBmTweT3wx@K$<)9Q&MU1>OoDaPAJUaM{Kje%d>$BMDOAkeXpQg_v!�dq+G|FaC7tve2=0M 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

Ic@!@-#yO-uf7v=;PcF!>56olpI>RpuZOy01` zYN4;XfXMLx-TZ4mSa zl|P`PX;sl0RWQEhz%YM|(RJ9o|PEl1SgJNXRZjAR=eP9vsI()zhtkZ3L1i zb)6)SD|}%wpXlMeHwv@bTW))uj=!5au=15SF@h0(+cBmXaK0MpvGXCB42q9{PI6U9 zl@M2-uZ1*i=td;lMSUN1RhvKV*63P4uJed&P-`SoMz zFzxZN1}TBnBWC?+fiy=h7da;aG&_=W5PN}nnZu~^aU zbqA&hH)j!o=TiRnAO#sRE!rh7rAD%c0H`l|&P9a?sDa z-&GG={Ql2b24s}L3;xEHfS4u3kVY`|?kp-V9To^X^#`55O8#xXz9@WHCHlz}`mLN? zwUw`UCUht=TAUm#KtmE0Sn=^gk0hJkiv7tLfMoZrD1+)Q4HCKr- zZzKIt{sT4k5yi6ROf%#6MYVd#synU5s5YQNaxu_^8gk*oqI+|HmhUcV{xxbMGKo7q zRHyQed@4W)bjRoE-EF>iulK8^R0B%4IVgxJz&=O7;n&qWybs_Z`#TN+4{LmwU9R

V<-Z<1O$V&1 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 Y?>zmqaBg-R69W) { + const { t } = useTranslation(); + + return ( + +

+ + {t('analyze:overview')} + + + # + + +

+ + + ); +}; +const CollaborationDataView = () => { + const { t } = useTranslation(); + return ( + <> + + + } + id={Topic.Productivity} + > + {t('analyze:topic.productivity')} + + + + + } + id={Topic.Robustness} + > + {t('analyze:topic.robustness')} + + + + } + id={Topic.NicheCreation} + > + {t('analyze:topic.niche_creation')} + + + + ); +}; + +export default Charts; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeMergeRatio.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeMergeRatio.tsx new file mode 100644 index 000000000..ab894d85e --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeMergeRatio.tsx @@ -0,0 +1,148 @@ +import React, { useMemo, useState } from 'react'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import EChartX from '@common/components/EChartX'; +import { TransOpt } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { + ChartDataProvider, + ChartOptionProvider, + useCardManual, + useOptionBuilderFns, + getRatioLineBuilder, + getCompareStyleBuilder, +} from '@modules/developer/options'; + +const CodeMergeRatio = () => { + const { t } = useTranslation(); + + const tabOptions = [ + { label: t('analyze:code_merge_ratio'), value: '1' }, + { label: t('analyze:total_pr'), value: '2' }, + { label: t('analyze:code_merge'), value: '3' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:code_merge_ratio'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.codeMergeRatio', + summaryKey: 'summaryCodequality.codeMergeRatio', + }, + '2': { + legendName: t('analyze:total_pr'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prCount', + summaryKey: 'summaryCodequality.prCount', + }, + '3': { + legendName: t('analyze:code_merge'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.codeMergedCount', + summaryKey: 'summaryCodequality.codeMergedCount', + }, + }; + + type TabValue = keyof typeof chartTabs; + const [tab, setTab] = useState('1'); + const tansOpts: TransOpt = chartTabs[tab]; + + const { + showMedian, + setShowMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useCardManual(); + + const geOptionFn = useOptionBuilderFns([ + getRatioLineBuilder({ + isRatio: tab === '1', + yAxisScale, + showMedian, + showAvg, + medianMame: t('analyze:median'), + avgName: t('analyze:average'), + }), + getCompareStyleBuilder({}), + ]); + + return ( + ( + { + setFullScreen(v); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + bodyRender={(ref, fullScreen) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + ( + + )} + /> + ); + }} + + + ); + }} + /> + ); +}; + +export default CodeMergeRatio; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeReviewRatio.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeReviewRatio.tsx new file mode 100644 index 000000000..f8302a8f6 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CodeReviewRatio.tsx @@ -0,0 +1,145 @@ +import React, { useMemo, useState } from 'react'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import { TabOption, TransOpt } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { + ChartDataProvider, + ChartOptionProvider, + useCardManual, + useOptionBuilderFns, + getRatioLineBuilder, + getCompareStyleBuilder, +} from '@modules/developer/options'; + +const CodeReviewRatio = () => { + const { t } = useTranslation(); + const tabOptions: TabOption[] = [ + { label: t('analyze:code_review_ratio'), value: '1' }, + { label: t('analyze:total_pr'), value: '2' }, + { label: t('analyze:code_review'), value: '3' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:code_review_ratio'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.codeReviewRatio', + summaryKey: 'summaryCodequality.codeReviewRatio', + }, + '2': { + legendName: t('analyze:total_pr'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prCount', + summaryKey: 'summaryCodequality.prCount', + }, + '3': { + legendName: t('analyze:code_review'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.codeReviewedCount', + summaryKey: 'summaryCodequality.codeReviewedCount', + }, + }; + + type TabValue = keyof typeof chartTabs; + + const [tab, setTab] = useState('1'); + const tansOpts: TransOpt = chartTabs[tab]; + + const { + showMedian, + setShowMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useCardManual(); + + const geOptionFn = useOptionBuilderFns([ + getRatioLineBuilder({ + isRatio: tab === '1', + yAxisScale, + showMedian, + showAvg, + medianMame: t('analyze:median'), + avgName: t('analyze:average'), + }), + getCompareStyleBuilder({}), + ]); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + bodyRender={(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + ( + + )} + /> + ); + }} + + + ); + }} + /> + ); +}; + +export default CodeReviewRatio; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitFrequency.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitFrequency.tsx new file mode 100644 index 000000000..a554ef9d1 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitFrequency.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { + ChartDataProvider, + ChartOptionProvider, + useCardManual, + useOptionBuilderFns, + getRatioLineBuilder, + getCompareStyleBuilder, + getLineBuilder, +} from '@modules/developer/options'; + +const CommitFrequency = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:collaboration_development_index.metrics.commit_frequency' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.commitFrequency', + summaryKey: 'summaryCodequality.commitFrequency', + }; + + const { + showMedian, + setShowMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useCardManual(); + + const geOptionFn = useOptionBuilderFns([ + getLineBuilder({ + yAxisScale, + showMedian, + showAvg, + medianMame: t('analyze:median'), + avgName: t('analyze:average'), + }), + getCompareStyleBuilder({}), + ]); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, result }) => { + return ( + { + return ( + + ); + }} + /> + ); + }} + + ); + }} + + ); +}; + +export default CommitFrequency; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitPRLinkedRatio.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitPRLinkedRatio.tsx new file mode 100644 index 000000000..70ef78a03 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/CommitPRLinkedRatio.tsx @@ -0,0 +1,133 @@ +import React, { useMemo, useState } from 'react'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import { TransOpt } from '@modules/developer/type'; + +import useGetRatioLineOption from '@modules/developer/hooks/useGetRatioLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CommitPRLinkedRatio = () => { + const { t } = useTranslation(); + const [tab, setTab] = useState('1'); + + const tabOptions = [ + { label: t('analyze:commit_pr_linked_ratio'), value: '1' }, + { label: t('analyze:commit_pr'), value: '2' }, + { label: t('analyze:commit_pr_linked'), value: '3' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:commit_pr_linked_ratio'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.gitPrLinkedRatio', + summaryKey: 'summaryCodequality.gitPrLinkedRatio', + }, + '2': { + legendName: t('analyze:commit_pr'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prCommitCount', + summaryKey: 'summaryCodequality.prCommitCount', + }, + '3': { + legendName: t('analyze:commit_pr_linked'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prCommitLinkedCount', + summaryKey: 'summaryCodequality.prCommitLinkedCount', + }, + }; + + type TabValue = keyof typeof chartTabs; + const tansOpts: TransOpt = chartTabs[tab]; + const { + getOptions, + setShowMedian, + showMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetRatioLineOption({ tab }); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + + {({ option }) => { + return ( + + ); + }} + + ); + }} + + + ); + }} +
+ ); +}; + +export default CommitPRLinkedRatio; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/ContributorCount.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/ContributorCount.tsx new file mode 100644 index 000000000..b40306793 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/ContributorCount.tsx @@ -0,0 +1,140 @@ +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import Tab from '@common/components/Tab'; +import EChartX from '@common/components/EChartX'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import { TransOpt } from '@modules/developer/type'; + +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const ContributorCount = () => { + const { t } = useTranslation(); + const [tab, setTab] = useState('1'); + + const chartTabs = { + '1': { + legendName: t('analyze:total'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.contributorCount', + summaryKey: 'summaryCodequality.contributorCount', + }, + '2': { + legendName: t('analyze:code_reviewer'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.activeC1PrCommentsContributorCount', + summaryKey: 'summaryCodequality.activeC1PrCommentsContributorCount', + }, + '3': { + legendName: t('analyze:pr_creator'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.activeC1PrCreateContributorCount', + summaryKey: 'summaryCodequality.activeC1PrCreateContributorCount', + }, + '4': { + legendName: t('analyze:commit_author'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.activeC2ContributorCount', + summaryKey: 'summaryCodequality.activeC2ContributorCount', + }, + }; + + const tansOpts: TransOpt = chartTabs[tab]; + type TabValue = keyof typeof chartTabs; + + const tabOptions = [ + { label: t('analyze:total'), value: '1' }, + { label: t('analyze:code_reviewer'), value: '2' }, + { label: t('analyze:pr_creator'), value: '3' }, + { label: t('analyze:commit_author'), value: '4' }, + ]; + const { + getOptions, + setShowMedian, + showMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref, fullScreen) => { + return ( +
+
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + + {({ option }) => { + return ( + + ); + }} + + ); + }} + +
+ ); + }} +
+ ); +}; + +export default ContributorCount; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/IsMaintained.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/IsMaintained.tsx new file mode 100644 index 000000000..8a7a31726 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/IsMaintained.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const IsMaintained = () => { + const { t } = useTranslation(); + + const tansOpt = { + legendName: 'is maintained', + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.isMaintained', + summaryKey: 'summaryCodequality.isMaintained', + }; + const { + getOptions, + showAvg, + showMedian, + setShowAvg, + setShowMedian, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + cardRef={ref} + yKey={tansOpt['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default IsMaintained; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/LocFrequency.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/LocFrequency.tsx new file mode 100644 index 000000000..a8262bdc3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/LocFrequency.tsx @@ -0,0 +1,141 @@ +import React, { useMemo, useState } from 'react'; +import { + bar, + getBarOption, + getColorWithLabel, + getTooltipsFormatter, + legendFormat, +} from '@common/options'; +import { formatNegativeNumber, shortenAxisLabel } from '@common/utils/format'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import { GenChartOptions } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const LocFrequency = () => { + const { t } = useTranslation(); + const chartTabs = { + '1': { + legendName: t('analyze:lines_add'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.linesAddedFrequency', + summaryKey: 'summaryCodequality.linesAddedFrequency', + }, + '2': { + legendName: t('analyze:lines_remove'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.linesRemovedFrequency', + summaryKey: 'summaryCodequality.linesRemovedFrequency', + }, + }; + + type TabValue = keyof typeof chartTabs; + + const tabOptions = [ + { label: t('analyze:lines_add'), value: '1' }, + { label: t('analyze:lines_remove'), value: '2' }, + ]; + + const [tab, setTab] = useState('1'); + const tansOpts = chartTabs[tab]; + + const getOptions: GenChartOptions = ( + { xAxis, compareLabels, yResults }, + theme + ) => { + const series = yResults.map(({ label, level, data }) => { + const color = getColorWithLabel(theme, label); + return bar({ + name: label, + stack: label, + data: formatNegativeNumber(tab === '2', data), + color, + }); + }); + + return getBarOption({ + xAxisData: xAxis, + series, + legend: legendFormat(compareLabels), + tooltip: { + formatter: getTooltipsFormatter({ compareLabels }), + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: (value: any) => { + return shortenAxisLabel(value) as string; + }, + }, + }, + }); + }; + + return ( + ( + { + setFullScreen(b); + }} + cardRef={ref} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, option }) => { + return ( + + ); + }} + + + ); + }} +
+ ); +}; + +export default LocFrequency; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/PRIssueLinked.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/PRIssueLinked.tsx new file mode 100644 index 000000000..c7d9cb1bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/PRIssueLinked.tsx @@ -0,0 +1,122 @@ +import React, { useMemo, useState } from 'react'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; + +import useGetRatioLineOption from '@modules/developer/hooks/useGetRatioLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const PRIssueLinked = () => { + const { t } = useTranslation(); + const chartTabs = { + '1': { + legendName: t('analyze:linked_issue_ratio'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prIssueLinkedRatio', + summaryKey: 'summaryCodequality.prIssueLinkedRatio', + }, + '2': { + legendName: t('analyze:total_pr'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prCount', + summaryKey: 'summaryCodequality.prCount', + }, + '3': { + legendName: t('analyze:linked_issue'), + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.prIssueLinkedCount', + summaryKey: 'summaryCodequality.prIssueLinkedCount', + }, + }; + + type TabValue = keyof typeof chartTabs; + + const tabOptions = [ + { label: t('analyze:linked_issue_ratio'), value: '1' }, + { label: t('analyze:total_pr'), value: '2' }, + { label: t('analyze:linked_issue'), value: '3' }, + ]; + + const [tab, setTab] = useState('1'); + const tansOpts = chartTabs[tab]; + const { + getOptions, + setShowMedian, + showMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetRatioLineOption({ tab }); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, option }) => { + return ( + + ); + }} + + + ); + }} +
+ ); +}; + +export default PRIssueLinked; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/TotalScore.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/TotalScore.tsx new file mode 100644 index 000000000..7db1bb854 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/TotalScore.tsx @@ -0,0 +1,85 @@ +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import { CollaborationDevelopment } from '@modules/developer/components/SideBar/config'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import EChartX from '@common/components/EChartX'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const TotalScore = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + xKey: 'grimoireCreationDate', + yKey: 'metricCodequality.codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + summaryKey: 'summaryCodequality.codeQualityGuarantee', + }; + const { + getOptions, + onePointSys, + setOnePointSys, + showAvg, + setShowAvg, + showMedian, + setShowMedian, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ + enableDataFormat: true, + }); + + return ( + ( + <> + { + setOnePointSys(v); + }} + /> + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + onePointSys={onePointSys} + yKey={tansOpts['yKey']} + /> + + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default TotalScore; diff --git a/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/index.tsx b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/index.tsx new file mode 100644 index 000000000..17fc8cef1 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CollaborationDevelopmentIndex/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import SectionTitle from '@modules/developer/components/SectionTitle'; +import { Section } from '@modules/developer/components/SideBar/config'; + +import TotalScore from './TotalScore'; + +import ContributorCount from './ContributorCount'; +import CommitFrequency from './CommitFrequency'; +import IsMaintained from './IsMaintained'; +import PRIssueLinked from './PRIssueLinked'; +import CommitPRLinkedRatio from './CommitPRLinkedRatio'; +import CodeReviewRatio from './CodeReviewRatio'; +import CodeMergeRatio from './CodeMergeRatio'; +import LocFrequency from './LocFrequency'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import ConnectLine from '@modules/developer/components/ConnectLine'; + +const CollaborationDevelopmentIndexOverview = () => { + const { t } = useTranslation(); + return ( + <> + + {t('metrics_models:collaboration_development_index.title')} + + +
+ +
+ +
+ + + + + + + + + +
+ + ); +}; + +export default withErrorBoundary(CollaborationDevelopmentIndexOverview, { + FallbackComponent: ErrorFallback, + onError(error, info) { + console.log(error, info); + // Do something with the error + // E.g. log to an error logging client here + }, +}); diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/CodeReviewCount.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/CodeReviewCount.tsx new file mode 100644 index 000000000..70c26a1ed --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/CodeReviewCount.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import { useTranslation } from 'next-i18next'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CodeReviewCount = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.code_review_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.codeReviewCount', + summaryKey: 'summaryActivity.codeReviewCount', + }; + const { + showAvg, + setShowAvg, + showMedian, + setShowMedian, + getOptions, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default CodeReviewCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/CommentFrequency.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/CommentFrequency.tsx new file mode 100644 index 000000000..f85d54486 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/CommentFrequency.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CommentFrequency = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.comment_frequency' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.commentFrequency', + summaryKey: 'summaryActivity.commentFrequency', + }; + const { + showAvg, + setShowAvg, + showMedian, + setShowMedian, + getOptions, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default CommentFrequency; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/CommitFrequency.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/CommitFrequency.tsx new file mode 100644 index 000000000..25d08f34e --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/CommitFrequency.tsx @@ -0,0 +1,79 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CommitFrequency = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t('metrics_models:community_activity.metrics.commit_frequency'), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.commitFrequency', + summaryKey: 'summaryActivity.commitFrequency', + }; + const { + getOptions, + setShowMedian, + showMedian, + showAvg, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default CommitFrequency; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/ContributorCount.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/ContributorCount.tsx new file mode 100644 index 000000000..97624313f --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/ContributorCount.tsx @@ -0,0 +1,85 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const ContributorCount = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.contributor_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.contributorCount', + summaryKey: 'summaryActivity.contributorCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default ContributorCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/OrgCount.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/OrgCount.tsx new file mode 100644 index 000000000..b84245ad1 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/OrgCount.tsx @@ -0,0 +1,82 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const OrgCount = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.organization_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.orgCount', + summaryKey: 'summaryActivity.orgCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default OrgCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/RecentReleasesCount.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/RecentReleasesCount.tsx new file mode 100644 index 000000000..4035c1f12 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/RecentReleasesCount.tsx @@ -0,0 +1,85 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const RecentReleasesCount = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.recent_releases_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.recentReleasesCount', + summaryKey: 'summaryActivity.recentReleasesCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default RecentReleasesCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScore.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScore.tsx new file mode 100644 index 000000000..eae66930d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScore.tsx @@ -0,0 +1,83 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const TotalScore = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t('metrics_models:community_activity.title'), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.activityScore', + summaryKey: 'summaryActivity.activityScore', + }; + const { + getOptions, + onePointSys, + setOnePointSys, + showAvg, + setShowAvg, + showMedian, + setShowMedian, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ + enableDataFormat: true, + }); + + return ( + ( + <> + { + setOnePointSys(v); + }} + /> + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + onePointSys={onePointSys} + yKey={tansOpts['yKey']} + /> + + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default TotalScore; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedIssuesCount.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedIssuesCount.tsx new file mode 100644 index 000000000..ba533a63f --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedIssuesCount.tsx @@ -0,0 +1,84 @@ +import React, { useMemo, useState } from 'react'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const UpdatedIssuesCount = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_activity.metrics.updated_issues_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.updatedIssuesCount', + summaryKey: 'summaryActivity.updatedIssuesCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default UpdatedIssuesCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedSince.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedSince.tsx new file mode 100644 index 000000000..3b737debc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/UpdatedSince.tsx @@ -0,0 +1,138 @@ +import React, { useMemo, useState } from 'react'; +import { EChartsOption } from 'echarts'; +import { Activity } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { getYAxisWithUnit } from '@common/options'; +import { convertMonthsToDays } from '@common/utils/format'; +import { DataContainerResult } from '@modules/developer/type'; + +// convert months to days. +const convertResult = (result: DataContainerResult) => { + result.summaryMean = result.summaryMean.map((value) => + convertMonthsToDays(value) + ); + result.summaryMedian = result.summaryMean.map((value) => + convertMonthsToDays(value) + ); + result.yResults = result.yResults.map((item) => { + item.data = item.data.map((value) => convertMonthsToDays(value)); + return item; + }); + return result; +}; + +const UpdatedSince = () => { + const { t, i18n } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t('metrics_models:community_activity.metrics.updated_since'), + xKey: 'grimoireCreationDate', + yKey: 'metricActivity.updatedSince', + summaryKey: 'summaryActivity.updatedSince', + }; + + const indicators = t('analyze:negative_indicators'); + const unit = t('analyze:unit_label', { + unit: t('analyze:unit_day'), + }); + + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ indicators }); + + const appendOptions = ( + options: EChartsOption, + result: DataContainerResult + ): EChartsOption => { + return { + ...options, + ...getYAxisWithUnit({ + result, + indicators, + unit, + namePaddingLeft: i18n.language === 'zh' ? 0 : 35, + shortenYaxisNumberLabel: true, + scale: yAxisScale, + }), + }; + }; + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, result }) => { + const convertData = convertResult(result); + return ( + + {({ option }) => { + const opts = appendOptions(option, result); + return ( + + ); + }} + + ); + }} + + ); + }} + + ); +}; + +export default UpdatedSince; diff --git a/apps/web/src/modules/developer/DataView/CommunityActivity/index.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/index.tsx new file mode 100644 index 000000000..bce7081ea --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import SectionTitle from '@modules/developer/components/SectionTitle'; +import { Section } from '@modules/developer/components/SideBar/config'; + +import TotalScore from './TotalScore'; + +import ContributorCount from './ContributorCount'; +import CommitFrequency from './CommitFrequency'; +import UpdatedSince from './UpdatedSince'; +import OrgCount from './OrgCount'; +import CommentFrequency from './CommentFrequency'; +import CodeReviewCount from './CodeReviewCount'; +import UpdatedIssuesCount from './UpdatedIssuesCount'; +import RecentReleasesCount from './RecentReleasesCount'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import ConnectLine from '@modules/developer/components/ConnectLine'; + +const CommunityActivity = () => { + const { t } = useTranslation(); + return ( + <> + + {t('metrics_models:activity.title')} + + +
+ +
+ +
+ + + + + + + + + +
+ + ); +}; + +export default withErrorBoundary(CommunityActivity, { + FallbackComponent: ErrorFallback, + onError(error, info) { + console.log(error, info); + // Do something with the error + // E.g. log to an error logging client here + }, +}); diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/BugIssueOpenTime.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/BugIssueOpenTime.tsx new file mode 100644 index 000000000..94a38ab82 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/BugIssueOpenTime.tsx @@ -0,0 +1,155 @@ +import React, { useMemo, useState } from 'react'; +import { EChartsOption } from 'echarts'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import { GenChartOptions, TransOpt } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import { getYAxisWithUnit } from '@common/options'; +import { DataContainerResult } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const BugIssueOpenTime = () => { + const { t, i18n } = useTranslation(); + + const tabOptions = [ + { label: t('analyze:average'), value: '1' }, + { label: t('analyze:median'), value: '2' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:average'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.issueOpenTimeAvg', + summaryKey: 'summaryCommunity.issueOpenTimeAvg', + }, + '2': { + legendName: t('analyze:median'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.issueOpenTimeMid', + summaryKey: 'summaryCommunity.issueOpenTimeMid', + }, + }; + + const [tab, setTab] = useState('1'); + type TabValue = keyof typeof chartTabs; + const tansOpts: TransOpt = chartTabs[tab]; + + const indicators = t('analyze:negative_indicators'); + const unit = t('analyze:unit_label', { + unit: t('analyze:unit_day'), + }); + + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ indicators }); + + const appendOptions = ( + options: EChartsOption, + result: DataContainerResult + ): EChartsOption => { + return { + ...options, + ...getYAxisWithUnit({ + result, + indicators, + unit, + namePaddingLeft: i18n.language === 'zh' ? 0 : 35, + scale: yAxisScale, + }), + }; + }; + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + + {({ option }) => { + const opts = appendOptions(option, result); + return ( + + ); + }} + + ); + }} + + + ); + }} +
+ ); +}; + +export default BugIssueOpenTime; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/ClosedPrsCount.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/ClosedPrsCount.tsx new file mode 100644 index 000000000..fa725497d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/ClosedPrsCount.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { + Activity, + Support, +} from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const ClosedPrsCount = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_service_and_support.metrics.close_pr_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.closedPrsCount', + summaryKey: 'summaryCommunity.closedPrsCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default ClosedPrsCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CodeReviewCount.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CodeReviewCount.tsx new file mode 100644 index 000000000..4b5d8e4e6 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CodeReviewCount.tsx @@ -0,0 +1,85 @@ +import React, { useMemo } from 'react'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CodeReviewCount = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_service_and_support.metrics.code_review_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.codeReviewCount', + summaryKey: 'summaryCommunity.codeReviewCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default CodeReviewCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CommentFrequency.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CommentFrequency.tsx new file mode 100644 index 000000000..d4e9d05a3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CommentFrequency.tsx @@ -0,0 +1,85 @@ +import React, { useMemo } from 'react'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const CommentFrequency = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: t( + 'metrics_models:community_service_and_support.metrics.comment_frequency' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.commentFrequency', + summaryKey: 'summaryCommunity.commentFrequency', + }; + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default CommentFrequency; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/IssueFirstResponse.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/IssueFirstResponse.tsx new file mode 100644 index 000000000..ed76c3d09 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/IssueFirstResponse.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { EChartsOption } from 'echarts'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import { DataContainerResult } from '@modules/developer/type'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { getYAxisWithUnit } from '@common/options'; + +const IssueFirstResponse = () => { + const { t, i18n } = useTranslation(); + const tabOptions = [ + { label: t('analyze:average'), value: '1' }, + { label: t('analyze:median'), value: '2' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:average'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.issueFirstReponseAvg', + summaryKey: 'summaryCommunity.issueFirstReponseAvg', + }, + '2': { + legendName: t('analyze:median'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.issueFirstReponseMid', + summaryKey: 'summaryCommunity.issueFirstReponseMid', + }, + }; + + type TabValue = keyof typeof chartTabs; + + const [tab, setTab] = useState('1'); + const tansOpts: TransOpt = chartTabs[tab]; + + const indicators = t('analyze:negative_indicators'); + const unit = t('analyze:unit_label', { + unit: t('analyze:unit_day'), + }); + + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ indicators }); + + const appendOptions = ( + options: EChartsOption, + result: DataContainerResult + ): EChartsOption => { + return { + ...options, + ...getYAxisWithUnit({ + result, + indicators, + unit, + namePaddingLeft: i18n.language === 'zh' ? 0 : 35, + scale: yAxisScale, + }), + }; + }; + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + + {({ option }) => { + const opts = appendOptions(option, result); + return ( + + ); + }} + + ); + }} + + + ); + }} +
+ ); +}; + +export default IssueFirstResponse; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/PrOpenTime.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/PrOpenTime.tsx new file mode 100644 index 000000000..234b0fb46 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/PrOpenTime.tsx @@ -0,0 +1,156 @@ +import React, { useMemo, useState } from 'react'; +import { EChartsOption } from 'echarts'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import { ChartDataProvider } from '@modules/developer/options'; +import ChartOptionContainer from '@modules/developer/components/Container/ChartOptionContainer'; +import { + TransOpt, + GenChartOptions, + DataContainerResult, +} from '@modules/developer/type'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import Tab from '@common/components/Tab'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { getYAxisWithUnit } from '@common/options'; + +const PrOpenTime = () => { + const { t, i18n } = useTranslation(); + + const tabOptions = [ + { label: t('analyze:average'), value: '1' }, + { label: t('analyze:median'), value: '2' }, + ]; + + const chartTabs = { + '1': { + legendName: t('analyze:average'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.prOpenTimeAvg', + summaryKey: 'summaryCommunity.prOpenTimeAvg', + }, + '2': { + legendName: t('analyze:median'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.prOpenTimeMid', + summaryKey: 'summaryCommunity.prOpenTimeMid', + }, + }; + + type TabValue = keyof typeof chartTabs; + + const [tab, setTab] = useState('1'); + const tansOpts: TransOpt = chartTabs[tab]; + + const indicators = t('analyze:negative_indicators'); + const unit = t('analyze:unit_label', { + unit: t('analyze:unit_day'), + }); + + const { + getOptions, + showAvg, + showMedian, + setShowMedian, + setShowAvg, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ indicators }); + + const appendOptions = ( + options: EChartsOption, + result: DataContainerResult + ): EChartsOption => { + return { + ...options, + ...getYAxisWithUnit({ + indicators, + unit, + namePaddingLeft: i18n.language === 'zh' ? 0 : 35, + result, + scale: yAxisScale, + }), + }; + }; + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + bodyClass={'h-[400px]'} + > + {(ref) => { + return ( + <> +
+ setTab(v as TabValue)} + /> +
+ + {({ loading, result }) => { + return ( + + {({ option }) => { + const opts = appendOptions(option, result); + return ( + + ); + }} + + ); + }} + + + ); + }} +
+ ); +}; + +export default PrOpenTime; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScore.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScore.tsx new file mode 100644 index 000000000..652ca205a --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScore.tsx @@ -0,0 +1,86 @@ +import React, { useMemo, useState } from 'react'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import EChartX from '@common/components/EChartX'; +import { useTranslation } from 'next-i18next'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const TotalScore = () => { + const { t } = useTranslation(); + + const tansOpts: TransOpt = { + legendName: t('metrics_models:community_service_and_support.title'), + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.communitySupportScore', + summaryKey: 'summaryCommunity.communitySupportScore', + }; + const { + getOptions, + onePointSys, + setOnePointSys, + showAvg, + setShowAvg, + showMedian, + setShowMedian, + yAxisScale, + setYAxisScale, + } = useGetLineOption({ + enableDataFormat: true, + }); + + return ( + ( + <> + { + setOnePointSys(v); + }} + /> + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + onePointSys={onePointSys} + yKey={tansOpts['yKey']} + /> + + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default TotalScore; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/UpdatedIssuesCount.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/UpdatedIssuesCount.tsx new file mode 100644 index 000000000..472c38a65 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/UpdatedIssuesCount.tsx @@ -0,0 +1,84 @@ +import React, { useMemo } from 'react'; +import { Support } from '@modules/developer/components/SideBar/config'; +import BaseCard from '@common/components/BaseCard'; +import { TransOpt, GenChartOptions } from '@modules/developer/type'; +import ChartWithData from '@modules/developer/components/ChartWithData'; +import EChartX from '@common/components/EChartX'; + +import { useTranslation } from 'next-i18next'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; + +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const UpdatedIssuesCount = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: 'updated issues count', + xKey: 'grimoireCreationDate', + yKey: 'metricCommunity.updatedIssuesCount', + summaryKey: 'summaryCommunity.updatedIssuesCount', + }; + const { + getOptions, + showAvg, + showMedian, + setShowAvg, + setShowMedian, + yAxisScale, + setYAxisScale, + } = useGetLineOption(); + + return ( + ( + { + setFullScreen(b); + }} + showAvg={showAvg} + onAvgChange={(b) => setShowAvg(b)} + showMedian={showMedian} + onMedianChange={(b) => setShowMedian(b)} + yAxisScale={yAxisScale} + onYAxisScaleChange={(b) => setYAxisScale(b)} + yKey={tansOpts['yKey']} + /> + )} + > + {(ref) => { + return ( + + {({ loading, option }) => { + return ( + + ); + }} + + ); + }} + + ); +}; + +export default UpdatedIssuesCount; diff --git a/apps/web/src/modules/developer/DataView/CommunityServiceSupport/index.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/index.tsx new file mode 100644 index 000000000..736491e4b --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; + +import SectionTitle from '@modules/developer/components/SectionTitle'; +import { Section } from '@modules/developer/components/SideBar/config'; + +import TotalScore from './TotalScore'; + +import IssueFirstResponse from './IssueFirstResponse'; +import BugIssueOpenTime from './BugIssueOpenTime'; +import CommentFrequency from './CommentFrequency'; +import UpdatedIssuesCount from './UpdatedIssuesCount'; + +import PrOpenTime from './PrOpenTime'; +import CodeReviewCount from './CodeReviewCount'; +import ClosedPrsCount from './ClosedPrsCount'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import ConnectLine from '@modules/developer/components/ConnectLine'; + +const CommunitySupport = () => { + const { t } = useTranslation(); + return ( + <> + + {t('metrics_models:community_service_and_support.title')} + + +
+ +
+ +
+ + + + + + + + +
+ + ); +}; + +export default withErrorBoundary(CommunitySupport, { + FallbackComponent: ErrorFallback, + onError(error, info) { + console.log(error, info); + // Do something with the error + // E.g. log to an error logging client here + }, +}); diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricChart.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricChart.tsx new file mode 100644 index 000000000..c54b40b6e --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricChart.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useEffect, useCallback } from 'react'; +import { useDeepCompareEffect } from 'ahooks'; +import { init, getInstanceByDom } from 'echarts'; +import type { CSSProperties } from 'react'; +import type { EChartsOption, ECharts, SetOptionOpts } from 'echarts'; +import { useResizeDetector } from 'react-resize-detector'; +import useInViewportDebounce from '@common/hooks/useInViewportDebounce'; + +export interface ReactEChartsProps { + option: EChartsOption; + style?: CSSProperties; + settings?: SetOptionOpts; + loading?: boolean; + theme?: 'light' | 'dark'; + containerRef?: React.RefObject; + filterData?: any; +} + +const MetricChart: React.FC = ({ + option, + style, + settings = { notMerge: true }, + loading, + theme, + containerRef, + filterData, +}) => { + const inView = useInViewportDebounce(containerRef); + const chartRef = useRef(null); + + useEffect(() => { + // Update chart + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current)!; + if (inView) { + loading === true ? chart?.showLoading() : chart?.hideLoading(); + } + } + }, [loading, inView]); + + useEffect(() => { + // init + let chart: ECharts | undefined; + if (chartRef.current !== null) { + chart = init(chartRef.current, theme); + } + return () => { + chart?.dispose(); + }; + }, [theme]); + + useDeepCompareEffect(() => { + // Update chart + if (inView && chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current)!; + chart.setOption(option, settings); + if (filterData) { + chart.on('legendselectchanged', function (params: any) { + const selected = params.selected!; + const options = chart.getOption(); + const selectedList = Object.keys(selected).filter( + (item) => selected[item] + ); + options.series[1].data = filterData.filter((item) => + selectedList.includes(item.parentName) + ); + chart.setOption(options); + }); + } + } + }, [option, settings, inView]); + + // ----------------container resize------------------------------ + const onResize = useCallback((width?: number, height?: number) => { + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current)!; + chart?.resize(); + } + }, []); + + useResizeDetector({ + targetRef: containerRef, + onResize, + skipOnMount: true, + }); + + return ( +
+ ); +}; + +export default React.memo(MetricChart); diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorContribution.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorContribution.tsx new file mode 100644 index 000000000..05be970f7 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorContribution.tsx @@ -0,0 +1,431 @@ +import React, { useRef, useMemo, useState } from 'react'; +import { + useEcoContributorsOverviewQuery, + useOrgContributionDistributionQuery, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import { useGetEcologicalText } from '../contribution'; +import { gradientRamp } from '@common/options'; +import PieDropDownMenu from '../../PieDropDownMenu'; +import type { EChartsOption } from 'echarts'; + +const ContributorContribution: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const [orgModel, setOrgModel] = useState(true); + const [onlyIdentity, setOnlyIdentity] = useState(false); + const [onlyOrg, setOnlyOrg] = useState(false); + + return ( +
+
+ { + setOrgModel(b); + if (b) { + setOnlyIdentity(false); + setOnlyOrg(false); + } + }} + onlyIdentity={onlyIdentity} + onOnlyIdentityChange={(b) => { + setOnlyIdentity(b); + if (b) { + setOrgModel(false); + setOnlyOrg(false); + } + }} + onlyOrg={onlyOrg} + onOnlyOrgChange={(b) => { + setOnlyOrg(b); + if (b) { + setOnlyIdentity(false); + setOrgModel(false); + } + }} + /> +
+ {orgModel ? ( + + ) : ( + + )} +
+ ); +}; + +const getSeriesFun = (data, onlyIdentity, onlyOrg, getEcologicalText) => { + const legend = []; + const ecoData = []; + const contributorsData = []; + let allCount = 0; + + if (onlyIdentity || onlyOrg) { + if (data?.ecoContributorsOverview?.length > 0) { + const ecoContributorsOverview = data.ecoContributorsOverview; + const map = onlyIdentity + ? ['manager', 'participant'] + : ['individual', 'organization']; + + map.forEach((item, i) => { + let list = ecoContributorsOverview.filter((i) => + i?.subTypeName?.includes(item) + ); + let distribution = list.flatMap((i) => i.topContributorDistribution); + distribution.sort((a, b) => b.subCount - a.subCount); + const { name, index } = getEcologicalText(item); + const colorList = gradientRamp[index]; + let count = 0; + let otherCount = 0; + if (item === 'organization') { + distribution = distribution.reduce((acc, curr) => { + const found = acc.find((item) => item.subBelong === curr.subBelong); + if (found) { + found.subCount += curr.subCount; + } else { + acc.push({ + subBelong: curr.subBelong, + subName: curr.subBelong, + subCount: curr.subCount, + }); + } + return acc; + }, []); + } + + distribution.forEach((z, index) => { + const { subCount, subName } = z; + count += subCount; + if (subName == 'other' || index > 10) { + otherCount += subCount; + } else { + contributorsData.push({ + parentName: name, + name: subName, + value: subCount, + itemStyle: { color: colorList[index + 1] }, + }); + } + }); + otherCount && + contributorsData.push({ + parentName: name, + name: 'other', + value: otherCount, + itemStyle: { color: colorList[0] }, + }); + legend.push({ + index: index, + name: name, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: name, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + } else { + if (data?.ecoContributorsOverview?.length > 0) { + const ecoContributorsOverview = data.ecoContributorsOverview; + ecoContributorsOverview.forEach((item, i) => { + const { subTypeName, topContributorDistribution } = item; + const { name, index } = getEcologicalText(subTypeName); + const colorList = gradientRamp[index]; + let count = 0; + topContributorDistribution.forEach(({ subCount, subName }, index) => { + count += subCount; + contributorsData.push({ + parentName: name, + name: subName, + value: subCount, + itemStyle: { color: colorList[index + 1] }, + }); + }); + legend.push({ + index: index, + name: name, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: name, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + } + legend.sort((a, b) => a?.index - b?.index); + return { + legend, + allCount, + ecoData, + contributorsData, + }; +}; + +const EcoContributorContribution: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; + orgModel: boolean; + onlyIdentity: boolean; + onlyOrg: boolean; +}> = ({ + label, + level, + beginDate, + endDate, + commonFilterOpts, + orgModel, + onlyIdentity, + onlyOrg, +}) => { + const { t } = useTranslation(); + const chartRef = useRef(null); + const { data, isLoading } = useEcoContributorsOverviewQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + // filterOpts: commonFilterOpts, + }); + const getEcologicalText = useGetEcologicalText(); + const getSeries = useMemo(() => { + return getSeriesFun(data, onlyIdentity, onlyOrg, getEcologicalText); + }, [data, onlyIdentity, onlyOrg, getEcologicalText]); + const unit: string = t('analyze:metric_detail:contribution_unit'); + const formatter = '{b} : {c}' + unit + ' ({d}%)'; + const option: EChartsOption = { + tooltip: { + trigger: 'item', + formatter: formatter, + }, + legend: { + top: 40, + left: 'center', + data: getSeries.legend, + }, + title: { + text: + t('analyze:metric_detail:global_contribution_distribution') + + '(' + + getSeries.allCount + + unit + + ')', + left: 'center', + }, + series: [ + { + top: 15, + name: '生态类型', + type: 'pie', + selectedMode: 'single', + radius: [0, '40%'], + label: { + position: 'inner', + fontSize: 12, + color: '#333', + formatter: formatter, + show: false, + }, + labelLine: { + show: false, + }, + labelLayout: { + hideOverlap: false, + moveOverlap: 'shiftY', + }, + data: getSeries.ecoData, + }, + { + top: 15, + name: '贡献者', + type: 'pie', + radius: ['50%', '62%'], + labelLine: { + length: 30, + }, + label: { + formatter: formatter, + color: '#333', + }, + data: getSeries.contributorsData, + }, + ], + }; + + return ( +
+ +
+ ); +}; + +const OrgContributorContribution: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const chartRef = useRef(null); + const { data, isLoading } = useOrgContributionDistributionQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + // filterOpts: commonFilterOpts, + }); + const getEcologicalText = useGetEcologicalText(); + const getSeries = useMemo(() => { + const legend = []; + const ecoData = []; + const contributorsData = []; + let allCount = 0; + if (data?.orgContributionDistribution?.length > 0) { + const orgContributionDistribution = data.orgContributionDistribution; + orgContributionDistribution.forEach((item, i) => { + const { subTypeName, topContributorDistribution } = item; + const { name, index } = getEcologicalText(subTypeName); + const colorList = gradientRamp[index]; + let count = 0; + topContributorDistribution.forEach(({ subCount, subName }, index) => { + count += subCount; + // const color = `rgba(74, 144, 226, ${1-index * 0.1})`; + contributorsData.push({ + parentName: name, + name: subName, + value: subCount, + itemStyle: { color: colorList[index + 1] }, + }); + }); + legend.push({ + index: index, + name: name, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: name, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + legend.sort((a, b) => a?.index - b?.index); + return { + legend, + allCount, + ecoData, + contributorsData, + }; + }, [data, getEcologicalText]); + const unit: string = t('analyze:metric_detail:contribution_unit'); + const formatter = '{b} : {c}' + unit + ' ({d}%)'; + const option: EChartsOption = { + tooltip: { + trigger: 'item', + formatter: formatter, + }, + legend: { + top: 40, + left: 'center', + data: getSeries.legend, + }, + title: { + text: + t('analyze:metric_detail:global_contribution_distribution') + + '(' + + getSeries.allCount + + unit + + ')', + left: 'center', + }, + series: [ + { + top: 15, + name: '生态类型', + type: 'pie', + selectedMode: 'single', + radius: [0, '40%'], + label: { + position: 'inner', + fontSize: 12, + color: '#333', + formatter: formatter, + show: false, + }, + labelLine: { + show: false, + }, + labelLayout: { + hideOverlap: false, + moveOverlap: 'shiftY', + }, + data: getSeries.ecoData, + }, + { + top: 15, + name: '贡献者', + type: 'pie', + radius: ['50%', '62%'], + labelLine: { + length: 30, + }, + label: { + formatter: formatter, + color: '#333', + }, + data: getSeries.contributorsData, + }, + ], + }; + return ( +
+ +
+ ); +}; +export default ContributorContribution; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorOrganizations.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorOrganizations.tsx new file mode 100644 index 000000000..70e124238 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/ContributorOrganizations.tsx @@ -0,0 +1,143 @@ +import React, { useRef, useMemo } from 'react'; +import { useOrgContributorsOverviewQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import { gradientRamp } from '@common/options'; +import type { EChartsOption } from 'echarts'; + +const ContributorContribution: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const chartRef = useRef(null); + const { data, isLoading } = useOrgContributorsOverviewQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + // filterOpts: commonFilterOpts, + }); + const getSeries = useMemo(() => { + const legend = []; + const ecoData = []; + const contributorsData = []; + let allCount = 0; + if (data?.orgContributorsOverview?.length > 0) { + const ecoContributorsOverview = data.orgContributorsOverview; + ecoContributorsOverview + .filter((item) => item.subTypeName) + .forEach((item, i) => { + const { subTypeName, topContributorDistribution } = item; + const colorList = gradientRamp[i]; + let count = 0; + topContributorDistribution.forEach(({ subCount, subName }, index) => { + count += subCount; + contributorsData.push({ + parentName: subTypeName, + name: subName, + value: subCount, + itemStyle: { color: colorList[index + 1] }, + }); + }); + legend.push({ + name: subTypeName, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: subTypeName, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + return { + legend, + allCount, + ecoData, + contributorsData, + }; + }, [data]); + const unit: string = t('analyze:metric_detail:contribution_unit'); + const formatter = '{b} : {c}' + unit + ' ({d}%)'; + const option: EChartsOption = { + tooltip: { + trigger: 'item', + formatter: formatter, + }, + legend: { + type: 'scroll', + top: 40, + left: 'center', + data: getSeries.legend, + }, + title: { + text: + t('analyze:metric_detail:organization_contribution_distribution') + + '(' + + getSeries.allCount + + unit + + ')', + left: 'center', + }, + series: [ + { + top: 15, + name: '生态类型', + type: 'pie', + selectedMode: 'single', + radius: [0, '40%'], + label: { + position: 'inner', + fontSize: 12, + color: '#333', + formatter: formatter, + show: false, + }, + labelLine: { + show: false, + }, + labelLayout: { + hideOverlap: false, + moveOverlap: 'shiftY', + }, + data: getSeries.ecoData, + }, + { + top: 15, + name: '贡献者', + type: 'pie', + radius: ['50%', '62%'], + labelLine: { + length: 30, + }, + label: { + formatter: formatter, + color: '#333', + }, + data: getSeries.contributorsData, + }, + ], + }; + + return ( +
+ +
+ ); +}; +export default ContributorContribution; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/index.tsx new file mode 100644 index 000000000..8368b97ba --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributionCount/index.tsx @@ -0,0 +1,31 @@ +import React, { useRef, useMemo } from 'react'; +import ContributorContribution from './ContributorContribution'; +import ContributorOrganizations from './ContributorOrganizations'; + +const ContributionCount: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + return ( +
+ + +
+ ); +}; +export default ContributionCount; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorDropdown.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorDropdown.tsx new file mode 100644 index 000000000..ba8e488c5 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorDropdown.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +// import { AiOutlineSearch } from 'react-icons/ai'; +import { Button, Input } from 'antd'; +import { useTranslation } from 'next-i18next'; + +const ContributorDropdown = ({ + selectedKeys, + setSelectedKeys, + confirm, + placeholder, +}) => { + const { t } = useTranslation(); + const [contributor, setContributor] = useState(selectedKeys); + + const handleSearch = () => { + setSelectedKeys(contributor); + confirm(); + }; + const handleReset = () => { + setContributor([]); + }; + return ( +
e.stopPropagation()}> + setContributor([e.target.value])} + onPressEnter={() => handleSearch()} + style={{ marginBottom: 8, display: 'block' }} + /> +
+ + +
+
+ ); +}; + +export default ContributorDropdown; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx new file mode 100644 index 000000000..bb5ff4fb8 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { getIcons } from '../utils'; + +const DomainPersona = ({ name, origin }) => { + let icon = getIcons(origin, name); + let url = getHubUrl(origin, name); + return ( +
+
{icon}
+ {url ? ( + + {name} + + ) : ( + name + )} +
+ ); +}; + +const getHubUrl = (origin, name) => { + switch (origin) { + case 'github': + return 'https://github.com/' + name; + case 'gitee': + return 'https://gitee.com/' + name; + // return ; + default: + return null; + } +}; + +export default DomainPersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx new file mode 100644 index 000000000..17b1ca8f3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx @@ -0,0 +1,142 @@ +import React, { useState, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import { useGetContributionTypeI18n } from '../contribution'; +import { getDomainData, getIcons } from '../utils'; +import { toFixed } from '@common/utils'; +import classnames from 'classnames'; +import Popper from '@mui/material/Popper'; + +const PopperContent = ({ dataList, name, active, setActive, origin }) => { + const { t } = useTranslation(); + const activeItem = dataList + .find((item) => item.type === active) + ?.childern.sort((a, b) => b.contribution - a.contribution); + // const allType = ['Code', 'Code Admin', 'Issue', 'Issue Admin', 'Observe']; + return ( +
+
+ {getIcons(origin, name)} + {name + ' ' + t('analyze:metric_detail:domain_persona_details')} +
+
+
+ {dataList.map(({ type, color, contribution }) => { + return ( +
{ + setActive(type); + }} + className={classnames( + 'flex h-9 w-full cursor-pointer items-center justify-between border-b border-r bg-[#F6F6F6] last:border-b-0', + { '!border-r-0 !bg-[#FFFFFF]': active === type } + )} + > +
+
{type}
+
+ {contribution} +
+
+ ); + })} +
+
+
+ {activeItem?.map(({ text, contribution }) => { + return ( +
+
{text}
+
{contribution}
+
+ ); + })} +
+
+
+ ); +}; + +const DomainPersona = ({ maxDomain, dataList, name, origin }) => { + const contributionTypeMap = useGetContributionTypeI18n(); + const domainData = useMemo(() => { + return getDomainData(dataList, contributionTypeMap); + }, [dataList, contributionTypeMap]); + const [active, setActive] = useState(''); + const [popperOpen, togglePopperOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const handleClick = (event: React.MouseEvent, type) => { + setActive(type); + setAnchorEl(event.currentTarget); + togglePopperOpen(() => true); + }; + + return ( +
{ + setActive(''); + popperOpen && togglePopperOpen(() => false); + }} + > +
+ {domainData.map(({ type, color, contribution }) => { + const width = toFixed((contribution / maxDomain) * 100, 2); + return active === type ? ( +
+
+
+ ) : ( +
{ + handleClick(e, type); + }} + style={{ backgroundColor: color, width: `${width}%` }} + className="h-2 cursor-pointer" + >
+ ); + })} +
+ + + +
+ ); +}; + +export default DomainPersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx new file mode 100644 index 000000000..04e06ddb0 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import Tooltip from '@common/components/Tooltip'; +import { AiOutlineQuestionCircle } from 'react-icons/ai'; + +const RolePersona = () => { + const { t } = useTranslation(); + + return ( +
+ {t('analyze:metric_detail:role_persona')} + {t('analyze:metric_detail:role_persona_desc')}} + placement="right" + > + + + + +
+ ); +}; + +export default RolePersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx new file mode 100644 index 000000000..ace9d46a8 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx @@ -0,0 +1,412 @@ +import React, { useMemo, useState } from 'react'; +import { + useContributorsDetailListQuery, + ContributorDetail, + FilterOptionInput, + SortOptionInput, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import MyTable from '@common/components/Table'; +import classnames from 'classnames'; +import { + useContributionTypeLsit, + useEcologicalType, + useMileageOptions, +} from '../contribution'; +import { getMaxDomain } from '../utils'; +import { + getContributorPolling, + getContributorExport, +} from '../../tableDownload'; +import DomainPersona from './DomainPersona'; +import ContributorName from './ContributorName'; +import RolePersona from './RolePersona'; +import ContributorDropdown from './ContributorDropdown'; +import { useTranslation } from 'next-i18next'; +import Download from '@common/components/Table/Download'; +import { useRouter } from 'next/router'; +import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; +import Dialog from '@common/components/Dialog'; +import Tooltip from '@common/components/Tooltip'; +import ManageOrgEdit from '@common/components/OrgEdit/ManageOrgEdit'; +import useVerifyDetailRangeQuery from '@modules/analyze/hooks/useVerifyDetailRangeQuery'; +import { useIsCurrentUser } from '@modules/analyze/hooks/useIsCurrentUser'; +import { FiEdit } from 'react-icons/fi'; +import { GrClose } from 'react-icons/gr'; +import { AiOutlineSearch, AiFillFilter } from 'react-icons/ai'; +import getErrorMessage from '@common/utils/getErrorMessage'; +import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; +import toast from 'react-hot-toast'; +interface TableParams { + pagination?: TablePaginationConfig; + filterOpts?: FilterOptionInput[]; + sortOpts?: SortOptionInput; + filters?: Record; +} + +const MetricTable: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const [openConfirm, setOpenConfirm] = useState(false); + const [currentName, setCurrentName] = useState(''); + const [currentOrgName, setCurrentOrgName] = useState(''); + const [origin, setOrigin] = useState(''); + + const { data } = useVerifyDetailRangeQuery(); + const { isCurrentUser } = useIsCurrentUser(); + const ecologicalOptions = useEcologicalType(); + const mileageOptions = useMileageOptions(); + const filterMap = { + ecologicalType: 'ecological_type', + contributionTypeList: 'contribution_type', + }; + const router = useRouter(); + const { handleQueryParams } = useHandleQueryParams(); + + const queryFilterOpts = router.query?.filterOpts as string; + const defaultFilterOpts = queryFilterOpts ? JSON.parse(queryFilterOpts) : []; + const defaultSortOpts = router.query?.sortOpts + ? JSON.parse(router.query?.sortOpts as string) + : null; + const [filterOpts, setFilterOpts] = useState(defaultFilterOpts || []); + const filterContributionType = useMemo(() => { + return filterOpts.find((i) => i.type === 'contribution_type'); + }, [filterOpts]); + const [tableData, setData] = useState(); + const [tableParams, setTableParams] = useState({ + pagination: { + current: 1, + pageSize: 10, + showSizeChanger: true, + position: ['bottomCenter'], + showTotal: (total) => { + return `${t('analyze:total_people', { total })} `; + }, + }, + sortOpts: defaultSortOpts, + }); + const query = { + page: tableParams.pagination.current, + per: tableParams.pagination.pageSize, + filterOpts: [...filterOpts, ...commonFilterOpts], + sortOpts: tableParams.sortOpts, + label, + level, + beginDate, + endDate, + }; + + const maxDomain = useMemo(() => { + return getMaxDomain(tableData); + }, [tableData]); + const { isLoading, isFetching } = useContributorsDetailListQuery( + client, + query, + { + onSuccess: (data) => { + const items = data.contributorsDetailList.items; + const hasTypeFilter = filterOpts.find( + (i) => i.type === 'contribution_type' + ); + if (hasTypeFilter) { + let value = hasTypeFilter.values; + items.map((item) => { + let list = item.contributionTypeList; + item.contributionTypeList = list.filter((i) => { + if (value.includes(i.contributionType)) { + return true; + } + }); + }); + } + setTableParams({ + ...tableParams, + pagination: { + ...tableParams.pagination, + total: data.contributorsDetailList.count, + }, + }); + setData(items); + setOrigin(data.contributorsDetailList.origin); + }, + onError: (e) => { + toast.error(getErrorMessage(e) || 'failed'); + }, + } + ); + const handleTableChange = ( + pagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult + ) => { + let sortOpts = null; + let filterOpts = []; + for (const key in filters) { + if (filters.hasOwnProperty(key)) { + const transformedObj = { + type: filterMap[key] || key, + values: filters[key] as string[], + }; + filters[key] && filterOpts.push(transformedObj); + } + } + if (filterOpts.find((i) => i.type === 'contribution_type')) { + sortOpts = sorter.order && { + type: + sorter.field === 'contribution' + ? 'contribution_filterd' + : sorter.field, + direction: sorter.order === 'ascend' ? 'asc' : 'desc', + }; + } else { + sortOpts = sorter.order && { + type: sorter.field, + direction: sorter.order === 'ascend' ? 'asc' : 'desc', + }; + } + handleQueryParams({ + filterOpts: filterOpts.length > 0 ? JSON.stringify(filterOpts) : null, + sortOpts: sortOpts && JSON.stringify(sortOpts), + }); + setFilterOpts(filterOpts); + setTableParams({ + pagination: { + showTotal: tableParams.pagination.showTotal, + ...pagination, + }, + sortOpts, + }); + }; + + const columns: ColumnsType = [ + { + title: t('analyze:metric_detail:contributor'), + dataIndex: 'contributor', + align: 'left', + width: '200px', + fixed: 'left', + render: (name) => { + return ; + }, + filterIcon: (filtered: boolean) => ( + + ), + defaultFilteredValue: + defaultFilterOpts.find((i) => i.type === 'contributor')?.values || null, + filterDropdown: ({ selectedKeys, setSelectedKeys, confirm }) => { + return ( + + ); + }, + }, + { + title: , + dataIndex: 'ecologicalType', + align: 'left', + width: '200px', + filters: ecologicalOptions, + defaultFilteredValue: + defaultFilterOpts.find((i) => i.type === 'ecological_type')?.values || + null, + render: (text) => { + return ecologicalOptions.find((i) => i.value === text)?.text || text; + }, + }, + + { + title: t('analyze:metric_detail:milestone_persona'), + dataIndex: 'mileageType', + render: (text) => { + return mileageOptions.find((i) => i.value === text)?.label || text; + }, + align: 'left', + width: '200px', + }, + { + title: t('analyze:metric_detail:domain_persona'), + dataIndex: 'contributionTypeList', + render: (dataList, col) => { + return ( + + ); + }, + filters: useContributionTypeLsit(), + defaultFilteredValue: + defaultFilterOpts.find((i) => i.type === 'contribution_type')?.values || + null, + filterMode: 'tree', + align: 'left', + width: '300px', + }, + { + title: t('analyze:metric_detail:organization'), + dataIndex: 'organization', + align: 'left', + width: '160px', + render: (text, col) => { + let edit = null; + if (isCurrentUser(col.contributor)) { + edit = ( + { + window.open('/settings/profile'); + }} + /> + ); + } else if (data?.verifyDetailDataRange?.status) { + edit = ( + { + setCurrentName(col.contributor); + col.organization && setCurrentOrgName(col.organization); + setOpenConfirm(true); + }} + /> + ); + } else { + edit = ( + {t('analyze:no_role_desc')}
} + placement="top" + > +
+ +
+ + ); + } + return ( +
+ {text || '-'} + {edit} +
+ ); + }, + filterIcon: (filtered: boolean) => ( + + ), + defaultFilteredValue: + defaultFilterOpts.find((i) => i.type === 'organization')?.values || + null, + filterDropdown: ({ selectedKeys, setSelectedKeys, confirm }) => { + return ( + + ); + }, + }, + { + title: t('analyze:metric_detail:contribution'), + dataIndex: 'contribution', + key: 'contribution', + render: (contribution, record) => { + if (filterContributionType) { + let filterCount = record.contributionTypeList.reduce( + (total, obj) => total + obj.contribution, + 0 + ); + return filterCount; + } else { + return contribution; + } + }, + align: 'left', + width: '120px', + sorter: true, + }, + ]; + return ( + <> +
+ +
+ + +

+ {currentName + + ' ' + + t('analyze:organization_information_modification')} +

+
{ + setOpenConfirm(false); + }} + > + +
+ + } + dialogContent={ +
+ { + setOpenConfirm(false); + }} + /> +
+ } + handleClose={() => { + setOpenConfirm(false); + }} + /> + + ); +}; +export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx new file mode 100644 index 000000000..e7544c81b --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx @@ -0,0 +1,237 @@ +import React, { useRef, useMemo, useState } from 'react'; +import { useContributorsOverviewQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import { gradientRamp } from '@common/options'; +import type { EChartsOption } from 'echarts'; +import { useGetEcologicalText } from './contribution'; +import PieDropDownMenu from '../PieDropDownMenu'; + +const getSeriesFun = (data, onlyIdentity, onlyOrg, getEcologicalText) => { + const legend = []; + const ecoData = []; + const contributorsData = []; + let allCount = 0; + + if (onlyIdentity || onlyOrg) { + if (data?.orgContributorsDistribution?.length > 0) { + const orgContributorsDistribution = data.orgContributorsDistribution; + const map = onlyIdentity + ? ['manager', 'participant'] + : ['individual', 'organization']; + + map.forEach((item, i) => { + let list = orgContributorsDistribution.filter((i) => + i?.subTypeName?.includes(item) + ); + let distribution = list.flatMap((i) => i.topContributorDistribution); + distribution.sort((a, b) => b.subCount - a.subCount); + const { name, index } = getEcologicalText(item); + const colorList = gradientRamp[index]; + let count = 0; + let otherCount = 0; + if (item === 'organization') { + distribution = distribution.reduce((acc, curr) => { + const found = acc.find((item) => item.subBelong === curr.subBelong); + if (found) { + found.subCount += curr.subCount; + } else { + acc.push({ + subBelong: curr.subBelong, + subName: curr.subBelong, + subCount: curr.subCount, + }); + } + return acc; + }, []); + } + + distribution.forEach((z, y) => { + const { subCount, subName } = z; + count += subCount; + if (subName == 'other' || y > 10) { + otherCount += subCount; + } else { + contributorsData.push({ + parentName: name, + name: subName, + value: subCount, + itemStyle: { color: colorList[y + 1] }, + }); + } + }); + otherCount && + contributorsData.push({ + parentName: name, + name: 'other', + value: otherCount, + itemStyle: { color: colorList[0] }, + }); + legend.push({ + index: index, + name: name, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: name, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + } else { + if (data?.orgContributorsDistribution?.length > 0) { + const orgContributorsDistribution = data.orgContributorsDistribution; + orgContributorsDistribution.forEach((item, i) => { + const { subTypeName, topContributorDistribution } = item; + const { name, index } = getEcologicalText(subTypeName); + const colorList = gradientRamp[index]; + let count = 0; + topContributorDistribution.forEach(({ subCount, subName }, index) => { + count += subCount; + contributorsData.push({ + parentName: name, + name: subName, + value: subCount, + itemStyle: { color: colorList[index + 1] }, + }); + }); + legend.push({ + name: name, + index: index, + itemStyle: { color: colorList[0] }, + }); + allCount += count; + ecoData.push({ + name: name, + value: count, + itemStyle: { color: colorList[0] }, + }); + }); + } + } + legend.sort((a, b) => a?.index - b?.index); + return { + legend, + allCount, + ecoData, + contributorsData, + }; +}; +const ContributorContributors: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const getEcologicalText = useGetEcologicalText(); + const chartRef = useRef(null); + const [onlyIdentity, setOnlyIdentity] = useState(false); + const [onlyOrg, setOnlyOrg] = useState(false); + const { data, isLoading } = useContributorsOverviewQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + // filterOpts: commonFilterOpts, + }); + + const getSeries = useMemo(() => { + return getSeriesFun(data, onlyIdentity, onlyOrg, getEcologicalText); + }, [data, onlyIdentity, onlyOrg, getEcologicalText]); + const unit: string = t('analyze:metric_detail:contributor_unit'); + const formatter = '{b} : {c}' + unit + ' ({d}%)'; + const option: EChartsOption = { + tooltip: { + trigger: 'item', + formatter: formatter, + }, + legend: { + top: 40, + left: 'center', + data: getSeries.legend, + }, + title: { + text: + t('analyze:metric_detail:contributor_distribution') + + '(' + + getSeries.allCount + + unit + + ')', + left: 'center', + }, + series: [ + { + top: 15, + name: '', + type: 'pie', + selectedMode: 'single', + radius: [0, '40%'], + label: { + position: 'inner', + fontSize: 12, + color: '#333', + formatter: formatter, + show: false, + }, + labelLine: { + show: false, + }, + labelLayout: { + hideOverlap: false, + moveOverlap: 'shiftY', + }, + data: getSeries.ecoData, + }, + { + top: 15, + name: '', + type: 'pie', + radius: ['50%', '62%'], + labelLine: { + length: 30, + }, + label: { + formatter: formatter, + color: '#333', + }, + data: getSeries.contributorsData, + }, + ], + }; + + return ( +
+
+ { + setOnlyIdentity(b); + if (b) { + setOnlyOrg(false); + } + }} + onlyOrg={onlyOrg} + onOnlyOrgChange={(b) => { + setOnlyOrg(b); + if (b) { + setOnlyIdentity(false); + } + }} + /> +
+ +
+ ); +}; +export default ContributorContributors; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts new file mode 100644 index 000000000..9a98e1b95 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts @@ -0,0 +1,234 @@ +import { useTranslation } from 'next-i18next'; + +// 领域画像 option +const useContributionTypeMap = () => { + const { t } = useTranslation(); + return { + Code: { + // code_author #code 编写 + // code_review #代码审查 + code_author: t('analyze:metric_detail:code:code_creation'), + code_review: t('analyze:metric_detail:code:code_review'), //delete + code_commit: t('analyze:metric_detail:code:code_commit'), //delete + pr_creation: t('analyze:metric_detail:code:pr_creation'), + pr_comments: t('analyze:metric_detail:code:pr_comments'), + }, + 'Code Admin': { + code_committer: t('analyze:metric_detail:code:code_committer'), + pr_labeled: t('analyze:metric_detail:code_admin:pr_labeled'), + pr_unlabeled: t('analyze:metric_detail:code_admin:pr_unlabeled'), + pr_closed: t('analyze:metric_detail:code_admin:pr_closed'), + pr_assigned: t('analyze:metric_detail:code_admin:pr_assigned'), + pr_unassigned: t('analyze:metric_detail:code_admin:pr_unassigned'), + pr_reopened: t('analyze:metric_detail:code_admin:pr_reopened'), + pr_milestoned: t('analyze:metric_detail:code_admin:pr_milestoned'), + pr_demilestoned: t('analyze:metric_detail:code_admin:pr_demilestoned'), + pr_marked_as_duplicate: t( + 'analyze:metric_detail:code_admin:pr_marked_as_duplicate' + ), + pr_transferred: t('analyze:metric_detail:code_admin:pr_transferred'), + pr_renamed_title: t('analyze:metric_detail:code_admin:pr_renamed_title'), + pr_change_description: t( + 'analyze:metric_detail:code_admin:pr_change_description' + ), + pr_setting_priority: t( + 'analyze:metric_detail:code_admin:pr_setting_priority' + ), + pr_change_priority: t( + 'analyze:metric_detail:code_admin:pr_change_priority' + ), + pr_merged: t('analyze:metric_detail:code_admin:pr_merged'), + pr_review: t('analyze:metric_detail:code_admin:pr_review'), + pr_set_tester: t('analyze:metric_detail:code_admin:pr_set_tester'), + pr_unset_tester: t('analyze:metric_detail:code_admin:pr_unset_tester'), + pr_check_pass: t('analyze:metric_detail:code_admin:pr_check_pass'), + pr_test_pass: t('analyze:metric_detail:code_admin:pr_test_pass'), + pr_reset_assign_result: t( + 'analyze:metric_detail:code_admin:pr_reset_assign_result' + ), + pr_reset_test_result: t( + 'analyze:metric_detail:code_admin:pr_reset_test_result' + ), + pr_link_issue: t('analyze:metric_detail:code_admin:pr_link_issue'), + pr_unlink_issue: t('analyze:metric_detail:code_admin:pr_unlink_issue'), + code_direct_commit: t( + 'analyze:metric_detail:code_admin:code_direct_commit' + ), //delete + }, + Issue: { + issue_creation: t('analyze:metric_detail:issue:issue_creation'), + issue_comments: t('analyze:metric_detail:issue:issue_comments'), + }, + 'Issue Admin': { + issue_labeled: t('analyze:metric_detail:issue_admin:issue_labeled'), + issue_unlabeled: t('analyze:metric_detail:issue_admin:issue_unlabeled'), + issue_closed: t('analyze:metric_detail:issue_admin:issue_closed'), + issue_reopened: t('analyze:metric_detail:issue_admin:issue_reopened'), + issue_assigned: t('analyze:metric_detail:issue_admin:issue_assigned'), + issue_unassigned: t('analyze:metric_detail:issue_admin:issue_unassigned'), + issue_milestoned: t('analyze:metric_detail:issue_admin:issue_milestoned'), + issue_demilestoned: t( + 'analyze:metric_detail:issue_admin:issue_demilestoned' + ), + issue_marked_as_duplicate: t( + 'analyze:metric_detail:issue_admin:issue_marked_as_duplicate' + ), + issue_transferred: t( + 'analyze:metric_detail:issue_admin:issue_transferred' + ), + issue_renamed_title: t( + 'analyze:metric_detail:issue_admin:issue_renamed_title' + ), + issue_change_description: t( + 'analyze:metric_detail:issue_admin:issue_change_description' + ), + issue_setting_priority: t( + 'analyze:metric_detail:issue_admin:issue_setting_priority' + ), + issue_change_priority: t( + 'analyze:metric_detail:issue_admin:issue_change_priority' + ), + issue_link_pull_request: t( + 'analyze:metric_detail:issue_admin:issue_link_pull_request' + ), + issue_unlink_pull_request: t( + 'analyze:metric_detail:issue_admin:issue_unlink_pull_request' + ), + issue_assign_collaborator: t( + 'analyze:metric_detail:issue_admin:issue_assign_collaborator' + ), + issue_unassign_collaborator: t( + 'analyze:metric_detail:issue_admin:issue_unassign_collaborator' + ), + issue_change_issue_state: t( + 'analyze:metric_detail:issue_admin:issue_change_issue_state' + ), + issue_change_issue_type: t( + 'analyze:metric_detail:issue_admin:issue_change_issue_type' + ), + issue_setting_branch: t( + 'analyze:metric_detail:issue_admin:issue_setting_branch' + ), + issue_change_branch: t( + 'analyze:metric_detail:issue_admin:issue_change_branch' + ), + }, + Observe: { + fork: t('analyze:metric_detail:observe:fork'), + star: t('analyze:metric_detail:observe:star'), + }, + }; +}; +// 领域画像 filter +export const useContributionTypeLsit = () => { + const obj = useContributionTypeMap(); + const result = []; + + for (const key in obj) { + const children = []; + for (const childKey in obj[key]) { + if ( + childKey !== 'code_commit' && + childKey !== 'code_direct_commit' && + childKey !== 'code_review' + ) { + children.push({ + text: obj[key][childKey], + value: childKey, + }); + } + } + result.push({ + text: key, + value: key, + children: children, + }); + } + return result; +}; +// 领域画像 i18n(表格字段翻译) +export const useGetContributionTypeI18n = () => { + const obj = useContributionTypeMap(); + const result = {}; + const colors = ['#4A90E2', '#9ECDF2', '#EAB308', '#FDE047', '#D1D5DB']; + const defaultColors = '#D1D5DB'; + function traverseObject(obj, color, type) { + for (const key in obj) { + if (typeof obj[key] === 'object') { + const c = colors.shift() || defaultColors; + traverseObject(obj[key], c, key); + } else { + result[key] = { text: obj[key], color, type }; + } + } + } + traverseObject(obj, defaultColors, null); + return result; +}; + +// 里程画像 i18n(表格字段翻译) +export const useMileageOptions = () => { + const { t } = useTranslation(); + + return [ + { label: t('analyze:metric_detail:core'), value: 'core' }, + { label: t('analyze:metric_detail:regular'), value: 'regular' }, + { label: t('analyze:metric_detail:guest'), value: 'guest' }, + ]; +}; + +//角色画像 option +export const useEcologicalType = () => { + const { t } = useTranslation(); + + return [ + { + text: t('analyze:metric_detail:organization_manager'), + value: 'organization manager', + }, + { + text: t('analyze:metric_detail:organization_participant'), + value: 'organization participant', + }, + { + text: t('analyze:metric_detail:individual_manager'), + value: 'individual manager', + }, + { + text: t('analyze:metric_detail:individual_participant'), + value: 'individual participant', + }, + ]; +}; +//角色画像 i18n +export const useGetEcologicalText = () => { + const { t } = useTranslation(); + const ecologicalOptions = useEcologicalType(); + const otherOptions = [ + { + text: t('analyze:metric_detail:organization'), + value: 'organization', + }, + { + text: t('analyze:metric_detail:individual'), + value: 'individual', + }, + + { + text: t('analyze:metric_detail:manager'), + value: 'manager', + }, + { + text: t('analyze:metric_detail:participant'), + value: 'participant', + }, + ]; + const options = [...ecologicalOptions, ...otherOptions]; + return (text) => { + const index = options.findIndex((i) => i.value === text); + return { + name: options[index]?.text || text, + index, + }; + }; +}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx new file mode 100644 index 000000000..0ceadc5ca --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx @@ -0,0 +1,207 @@ +import React, { useState, useMemo } from 'react'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import useVerifyDateRange from '../useVerifyDateRange'; +import { Checkbox } from 'antd'; +import { useTranslation } from 'next-i18next'; +import { useMileageOptions } from './contribution'; +import MetricTable from './ContributorTable'; +import ContributionCount from './ContributionCount'; +import ContributorContributors from './Contributors'; +import { AiOutlineQuestionCircle } from 'react-icons/ai'; +import Tooltip from '@common/components/Tooltip'; +import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; +import { useRouter } from 'next/router'; +import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; +import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; + +const MetricContributor = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { handleQueryParams } = useHandleQueryParams(); + const { verifiedItems } = useLabelStatus(); + const { label, level } = verifiedItems[0]; + const queryCard = router.query?.card as string; + const [tab, setTab] = useState(queryCard || '1'); + const { timeStart, timeEnd } = useVerifyDateRange(); + const options = useMileageOptions(); + const queryMileage = router.query?.mileage as string; + const defaultMileage = queryMileage + ? JSON.parse(queryMileage) + : ['core', 'regular']; + const [mileage, setMileage] = useState(defaultMileage); + const [isBot, setIsBot] = useState(false); + const [repoList, setRepoList] = useState([]); + + const onMileageChange = (checkedValues: string[]) => { + setMileage(checkedValues); + handleQueryParams({ mileage: JSON.stringify(checkedValues) }); + }; + const commonFilterOpts = useMemo(() => { + let opts = []; + if (mileage.length > 0) { + opts.push({ type: 'mileage_type', values: mileage }); + } + let botValues = ['false']; + if (isBot) { + botValues.push('true'); + } + opts.push({ type: 'is_bot', values: botValues }); + if (repoList.length > 0) { + opts.push({ type: 'repo_urls', values: repoList }); + } + return opts; + }, [mileage, isBot, repoList]); + let source; + switch (tab) { + case '1': { + source = ( + + ); + break; + } + case '2': { + source = ( + + ); + break; + } + case '3': { + source = ( + + ); + break; + } + default: { + source = ( + + ); + break; + } + } + return ( +
+
+ setIsBot(v)} + onRepoChange={(v) => setRepoList(v)} + /> +
+ + {t('analyze:metric_detail:milestone_persona_filter')} + +
+ + {t('analyze:metric_detail:core')} : + + {t('analyze:metric_detail:core_desc')} +
+
+ + {t('analyze:metric_detail:regular')} : + + {t('analyze:metric_detail:regular_desc')} +
+
+ + {t('analyze:metric_detail:guest')} : + + {t('analyze:metric_detail:guest_desc')} +
+ + } + placement="right" + > + + + +
+
+ : + +
+
+
+
+ { + setTab(v); + handleQueryParams({ card: v }); + }} + aria-label="Tabs where selection follows focus" + selectionFollowsFocus + > + + + + +
+ +
{source}
+
+
+ ); +}; + +export default MetricContributor; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx new file mode 100644 index 000000000..3d235f89d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx @@ -0,0 +1,92 @@ +import { IoPeopleCircle } from 'react-icons/io5'; +import Image from 'next/image'; + +export const getMaxDomain = (tableData) => { + if (tableData?.length > 0) { + const filterData = tableData?.map((item) => { + let filterCount = item?.contributionTypeList?.reduce( + (acc, current) => acc + current.contribution, + 0 + ); + return { ...item, filterCount }; + }); + let maxCountElement = filterData?.reduce((prev, current) => + prev?.filterCount > current.filterCount ? prev : current + ); + return maxCountElement.filterCount; + } else { + return 0; + } +}; +export const getDomainData = (data, contributionTypeMap) => { + let arr = data.map((item) => { + return { ...item, ...contributionTypeMap[item.contributionType] }; + }); + const result = []; + arr.forEach(({ color, contribution, text, type }) => { + const domainType = result.find((z) => z.type === type); + if (domainType) { + domainType.contribution += contribution; + domainType.childern.push({ text, contribution }); + } else { + result.push({ + type, + color, + contribution, + childern: [{ text, contribution }], + }); + } + }); + return result.sort((a, b) => { + if (a.type < b.type) { + return -1; + } + if (a.type > b.type) { + return 1; + } + return 0; + }); +}; + +export const getIcons = (origin, name) => { + switch (origin) { + case 'github': + return ( +
+ (e.currentTarget.src = '/images/github.png')} + unoptimized + fill={true} + style={{ + objectFit: 'cover', + }} + alt="icon" + placeholder="blur" + blurDataURL="/images/github.png" + /> +
+ ); + case 'gitee': + return ( +
+ + (e.currentTarget.src = '/images/logos/gitee-red.svg') + } + unoptimized + fill={true} + style={{ + objectFit: 'cover', + }} + alt="icon" + placeholder="blur" + blurDataURL="/images/logos/gitee-red.svg" + /> +
+ ); + default: + return ; + } +}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx new file mode 100644 index 000000000..e877f7e8f --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx @@ -0,0 +1,396 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import { IoPeopleCircle, IoPersonCircle } from 'react-icons/io5'; +import { GoIssueOpened, GoGitPullRequestClosed } from 'react-icons/go'; +import { + AiFillClockCircle, + AiOutlineIssuesClose, + AiOutlineArrowRight, +} from 'react-icons/ai'; +import { BiChat, BiGitPullRequest, BiGitCommit } from 'react-icons/bi'; +import useCompareItems from '@modules/analyze/hooks/useCompareItems'; +import useQueryDateRange from '@modules/analyze/hooks/useQueryDateRange'; +import { + useMetricDashboardQuery, + ContributorDetailOverview, + IssueDetailOverview, + PullDetailOverview, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { SiGitee, SiGithub } from 'react-icons/si'; +import { toFixed } from '@common/utils'; +import { useRouter } from 'next/router'; +import Image from 'next/image'; + +const MetricDashboard = () => { + const { compareItems } = useCompareItems(); + const len = compareItems.length; + if (len > 1) { + return null; + } + return
; +}; + +const Main = () => { + const { t } = useTranslation(); + const { compareItems } = useCompareItems(); + const { timeStart, timeEnd } = useQueryDateRange(); + const router = useRouter(); + const slugs = router.query.slugs; + const { label, level } = compareItems[0]; + const { data, isLoading } = useMetricDashboardQuery(client, { + label: label, + level: level, + beginDate: timeStart, + endDate: timeEnd, + }); + + if (isLoading) { + return ( + <> +
+
+ {t('analyze:metric_detail:project_deep_dive_insight')} +
+
{ + const query = window.location.search; + router.push('/analyze/insight/' + slugs + query); + }} + > + {t('analyze:metric_detail:details')} + +
+
+ + + ); + } + return ( +
+
+
+ {t('analyze:metric_detail:project_deep_dive_insight')} +
+
{ + const query = window.location.search; + router.push('/analyze/insight/' + slugs + query); + }} + > + {t('analyze:metric_detail:details')} + +
+
+
+ +
+ + +
+
+
+ ); +}; + +const MetricBoxContributors: React.FC<{ + data: ContributorDetailOverview; +}> = ({ data }) => { + const { t } = useTranslation(); + return ( +
+
+
+ {t('analyze:metric_detail:contributor')} +
+
+
+
+
+
+ +
+
{data.contributorAllCount}
+
+
+ {t('analyze:metric_detail:contributor_count')} +
+
+
+
+ {getTopUser( + data.highestContributionContributor.origin, + data.highestContributionContributor.name + )} +
+
+ {t('analyze:metric_detail:top_contributor')} +
+
+
+
+
+ +
+
{data.orgAllCount}
+
+
+ {t('analyze:metric_detail:org_count')} +
+
+
+
+
+ {getIcons(data.highestContributionOrganization.origin)} +
+
+ {data.highestContributionOrganization.name || '/'} +
+
+
+ {t('analyze:metric_detail:top_contributing_org')} +
+
+
+
+ ); +}; +const MetricBoxIssues: React.FC<{ + data: IssueDetailOverview; +}> = ({ data }) => { + const { t } = useTranslation(); + + return ( +
+
+
+ {t('analyze:metric_detail:issues')} +
+
+
+
+
+
+ +
+
{data.issueCount}
+
+
+ {t('analyze:metric_detail:newly_created_issues')} +
+
+
+
+
+ +
+
+ {data.issueCompletionRatio + ? toFixed(data.issueCompletionRatio * 100, 1) + + '% (' + + data.issueCompletionCount + + ')' + : '/'} +
+
+
+ {t('analyze:metric_detail:issue_completion_rate')} +
+
+
+
+
+ +
+
{data.issueUnresponsiveCount}
+
+
+ {t('analyze:metric_detail:unanswered_issue_count')} +
+
+
+
+
+ +
+
+ {toFixed(data.issueCommentFrequencyMean, 2)} +
+
+
+ {t('analyze:metric_detail:average_comments_count')} +
+
+
+
+ ); +}; + +const MetricBoxPr: React.FC<{ + data: PullDetailOverview; +}> = ({ data }) => { + const { t } = useTranslation(); + + return ( +
+
+
+ {t('analyze:metric_detail:pull_requests')} +
+
+
+
+
+
+ +
+
{data.pullCount}
+
+
+ {t('analyze:metric_detail:newly_created_pr_count')} +
+
+
+
+
+ +
+
+ {data.pullCompletionRatio + ? toFixed(data.pullCompletionRatio * 100, 1) + + '% (' + + data.pullCompletionCount + + ')' + : '/'} +
+
+
+ {t('analyze:metric_detail:pr_completion_rate')} +
+
+
+
+
+ +
+
{data.pullUnresponsiveCount}
+
+
+ {t('analyze:metric_detail:unanswered_pr_count')} +
+
+
+
+
+ +
+
{data.commitCount}
+
+
+ {t('analyze:metric_detail:commit_count')} +
+
+
+
+ ); +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +const getIcons = (type: string) => { + switch (type) { + case 'github': + return ; + case 'gitee': + return ; + default: + return ; + } +}; +const getTopUser = (type, name) => { + let url = null; + let userIcon = null; + if (!name) { + userIcon = ; + } else { + switch (type) { + case 'github': + url = 'https://github.com/' + name; + userIcon = ( +
+ (e.currentTarget.src = '/images/github.png')} + unoptimized + fill={true} + style={{ + objectFit: 'cover', + }} + alt="icon" + placeholder="blur" + blurDataURL="/images/github.png" + /> +
+ ); + break; + case 'gitee': + url = 'https://gitee.com/' + name; + userIcon = ( +
+ + (e.currentTarget.src = '/images/logos/gitee-red.svg') + } + unoptimized + fill={true} + alt="icon" + placeholder="blur" + blurDataURL="/images/logos/gitee-red.svg" + /> +
+ ); + break; + default: + userIcon = ; + break; + } + } + + return ( + <> +
{userIcon}
+
+ {url ? ( + + {name} + + ) : ( + name || '/' + )} +
+ + ); +}; +export default MetricDashboard; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx new file mode 100644 index 000000000..debeacff9 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx @@ -0,0 +1,50 @@ +import React, { useRef, useMemo } from 'react'; +import { useIssueCommentQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; + +const IssueCompletion: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; +}> = ({ label, level, beginDate, endDate }) => { + const { t } = useTranslation(); + const chartRef = useRef(null); + const { data, isLoading } = useIssueCommentQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + }); + + const getSeries = useMemo(() => { + const distribution = data?.issuesDetailOverview?.issueCommentDistribution; + if (data && distribution?.length > 0) { + return distribution.map(({ subCount, subName }) => { + return { + name: subName + t('analyze:metric_detail:comments'), + value: subCount, + count: subCount, + }; + }); + } else { + return []; + } + }, [data, t]); + + const option = getPieOption({ seriesData: getSeries }); + + return ( +
+ +
+ ); +}; +export default IssueCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx new file mode 100644 index 000000000..2b0cdbaf9 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx @@ -0,0 +1,54 @@ +import React, { useRef, useMemo } from 'react'; +import { useIssueCompletionQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import type { EChartsOption } from 'echarts'; +import { useStateType } from './issue'; +import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; + +const IssueCompletion: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; +}> = ({ label, level, beginDate, endDate }) => { + const { t } = useTranslation(); + const stateOption = useStateType(); + const chartRef = useRef(null); + const { data, isLoading } = useIssueCompletionQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + }); + const getStateText = (text) => { + return stateOption.find((i) => i.value === text)?.text || text; + }; + const getSeries = useMemo(() => { + const distribution = data?.issuesDetailOverview?.issueStateDistribution; + if (data && distribution?.length > 0) { + return distribution.map(({ subCount, subName }) => { + return { + name: getStateText(subName), + value: subCount, + }; + }); + } else { + return []; + } + }, [data, getStateText]); + + const option = getPieOption({ seriesData: getSeries }); + + return ( +
+ +
+ ); +}; +export default IssueCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx new file mode 100644 index 000000000..4ea9540c6 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react'; +import { + useIssuesDetailListQuery, + IssueDetail, + FilterOptionInput, + SortOptionInput, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import MyTable from '@common/components/Table'; +import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; +import { useTranslation } from 'next-i18next'; +import { format, parseJSON } from 'date-fns'; +import { useStateType } from './issue'; +import { toUnderline } from '@common/utils/format'; +import Download from '@common/components/Table/Download'; +import { getIssuePolling, getIssueExport } from '../tableDownload'; + +interface TableParams { + pagination?: TablePaginationConfig; + filterOpts?: FilterOptionInput[]; + sortOpts?: SortOptionInput; + filters?: Record; +} + +const MetricTable: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const stateOption = useStateType(); + const [tableData, setData] = useState(); + const [tableParams, setTableParams] = useState({ + pagination: { + current: 1, + pageSize: 10, + showSizeChanger: true, + position: ['bottomCenter'], + showTotal: (total) => { + return `${t('analyze:total_issues', { total })} `; + }, + }, + filterOpts: [], + sortOpts: { + type: 'state', + direction: 'desc', + }, + }); + + const query = { + page: tableParams.pagination.current, + per: tableParams.pagination.pageSize, + filterOpts: [...tableParams.filterOpts, ...commonFilterOpts], + sortOpts: tableParams.sortOpts, + label, + level, + beginDate, + endDate, + }; + const { isLoading, isFetching } = useIssuesDetailListQuery(client, query, { + // enabled: false, + onSuccess: (data) => { + const items = data.issuesDetailList.items; + setData(items); + setTableParams({ + ...tableParams, + pagination: { + ...tableParams.pagination, + total: data.issuesDetailList.count, + }, + }); + }, + }); + const handleTableChange = ( + pagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult + ) => { + let sortOpts = null; + let filterOpts = []; + sortOpts = sorter.field && { + type: toUnderline(sorter.field as string), + direction: sorter.order === 'ascend' ? 'asc' : 'desc', + }; + for (const key in filters) { + if (filters.hasOwnProperty(key)) { + const transformedObj = { + type: key, + values: filters[key] as string[], + }; + filters[key] && filterOpts.push(transformedObj); + } + } + setTableParams({ + pagination: { + showTotal: tableParams.pagination.showTotal, + ...pagination, + }, + sortOpts, + filterOpts, + }); + }; + + const columns: ColumnsType = [ + { + title: t('analyze:metric_detail:issue_title'), + dataIndex: 'title', + align: 'left', + width: '200px', + sorter: true, + fixed: 'left', + }, + { + title: 'URL', + dataIndex: 'url', + align: 'left', + width: '220px', + }, + { + title: t('analyze:metric_detail:state'), + dataIndex: 'state', + align: 'left', + width: '100px', + sorter: true, + filters: stateOption, + render: (text) => { + return stateOption.find((i) => i.value === text)?.text || text; + }, + }, + { + title: t('analyze:metric_detail:created_time'), + dataIndex: 'createdAt', + align: 'left', + sorter: true, + width: '140px', + render: (time) => format(parseJSON(time)!, 'yyyy-MM-dd'), + }, + { + title: t('analyze:metric_detail:close_time'), + dataIndex: 'closedAt', + align: 'left', + sorter: true, + width: '120px', + render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), + }, + { + title: t('analyze:metric_detail:processing_time'), + dataIndex: 'timeToCloseDays', + align: 'left', + sorter: true, + width: '200px', + }, + { + title: t('analyze:metric_detail:first_response_time'), + dataIndex: 'timeToFirstAttentionWithoutBot', + align: 'left', + sorter: true, + width: '220px', + }, + { + title: t('analyze:metric_detail:comments_count'), + dataIndex: 'numOfCommentsWithoutBot', + align: 'left', + sorter: true, + width: '160px', + }, + { + title: t('analyze:metric_detail:tags'), + dataIndex: 'labels', + align: 'left', + render: (list) => list?.join(', ') || '', + width: '100px', + }, + { + title: t('analyze:metric_detail:creator'), + dataIndex: 'userLogin', + align: 'left', + width: '100px', + }, + { + title: t('analyze:metric_detail:assignee'), + dataIndex: 'assigneeLogin', + align: 'left', + width: '100px', + }, + ]; + return ( + <> +
+ +
+ + + ); +}; +export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx new file mode 100644 index 000000000..bb3a5e9b4 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx @@ -0,0 +1,134 @@ +import React, { useState, useMemo } from 'react'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import useVerifyDateRange from '../useVerifyDateRange'; +import MetricTable from './IssueTable'; +import IssueCompletion from './IssueCompletion'; +import IssueComments from './IssueComments'; +import { useTranslation } from 'next-i18next'; +import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; +import { useRouter } from 'next/router'; +import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; +import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; + +const MetricIssue = () => { + const router = useRouter(); + const { handleQueryParams } = useHandleQueryParams(); + const { verifiedItems } = useLabelStatus(); + const { timeStart, timeEnd } = useVerifyDateRange(); + const { label, level } = verifiedItems[0]; + const { t } = useTranslation(); + const queryCard = router.query?.card as string; + const [repoList, setRepoList] = useState([]); + + const [tab, setTab] = useState(queryCard || '1'); + + const commonFilterOpts = useMemo(() => { + let opts = []; + if (repoList.length > 0) { + opts.push({ type: 'repo_urls', values: repoList }); + } + return opts; + }, [repoList]); + let source; + switch (tab) { + case '1': { + source = ( + + ); + break; + } + case '2': { + source = ( + + ); + break; + } + case '3': { + source = ( + + ); + break; + } + default: { + source = ( + + ); + break; + } + } + return ( +
+ setRepoList(v)} + level={level} + label={label} + type={'issue'} + /> + { + setTab(v); + handleQueryParams({ card: v }); + }} + aria-label="Tabs where selection follows focus" + selectionFollowsFocus + > + + + + + +
{source}
+
+ ); +}; + +export default MetricIssue; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts new file mode 100644 index 000000000..32cf630d3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; + +export const useStateType = () => { + const { t } = useTranslation(); + + return [ + { + text: t('analyze:metric_detail:open'), + value: 'open', + }, + { + text: t('analyze:metric_detail:closed'), + value: 'closed', + }, + { + text: t('analyze:metric_detail:progressing'), + value: 'progressing', + }, + { + text: t('analyze:metric_detail:rejected'), + value: 'rejected', + }, + ]; +}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts new file mode 100644 index 000000000..edbbe5ba4 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next'; + +export const useStateType = () => { + const { t } = useTranslation(); + + return [ + { + text: t('analyze:metric_detail:open'), + value: 'open', + }, + { + text: t('analyze:metric_detail:closed'), + value: 'closed', + }, + { + text: t('analyze:metric_detail:merged'), + value: 'merged', + }, + ]; +}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx new file mode 100644 index 000000000..aa99d974c --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx @@ -0,0 +1,49 @@ +import React, { useRef, useMemo } from 'react'; +import { usePullsCommentQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { useTranslation } from 'next-i18next'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import type { EChartsOption } from 'echarts'; +import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; + +const PrComments: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; +}> = ({ label, level, beginDate, endDate }) => { + const { t } = useTranslation(); + const chartRef = useRef(null); + const { data, isLoading } = usePullsCommentQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + }); + const getSeries = useMemo(() => { + const distribution = data?.pullsDetailOverview?.pullCommentDistribution; + if (data && distribution?.length > 0) { + return distribution.map(({ subCount, subName }) => { + return { + name: subName + t('analyze:metric_detail:comments'), + value: subCount, + }; + }); + } else { + return []; + } + }, [data, t]); + + const option = getPieOption({ seriesData: getSeries }); + + return ( +
+ +
+ ); +}; +export default PrComments; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx new file mode 100644 index 000000000..313cd255b --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx @@ -0,0 +1,48 @@ +import React, { useRef, useMemo } from 'react'; +import { usePullsCompletionQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; +import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; +import { useStateType } from '@modules/analyze/DataView/MetricDetail/MetricPr/PR'; + +const PrCompletion: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; +}> = ({ label, level, beginDate, endDate }) => { + const stateOption = useStateType(); + const chartRef = useRef(null); + const { data, isLoading } = usePullsCompletionQuery(client, { + label: label, + level: level, + beginDate: beginDate, + endDate: endDate, + }); + const getStateText = (text) => { + return stateOption.find((i) => i.value === text)?.text || text; + }; + const getSeries = useMemo(() => { + const distribution = data?.pullsDetailOverview?.pullStateDistribution; + if (data && distribution?.length > 0) { + return distribution.map(({ subCount, subName }) => { + return { name: getStateText(subName), value: subCount }; + }); + } else { + return []; + } + }, [data, getStateText]); + + const option = getPieOption({ seriesData: getSeries }); + + return ( +
+ +
+ ); +}; +export default PrCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx new file mode 100644 index 000000000..47d027895 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react'; +import { + usePullsDetailListQuery, + PullDetail, + FilterOptionInput, + SortOptionInput, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import MyTable from '@common/components/Table'; +import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; +import type { FilterValue, SorterResult } from 'antd/es/table/interface'; +import { useTranslation } from 'next-i18next'; +import { format, parseJSON } from 'date-fns'; +import { useStateType } from '@modules/analyze/DataView/MetricDetail/MetricPr/PR'; +import { toUnderline } from '@common/utils/format'; +import Download from '@common/components/Table/Download'; +import { getPrPolling, getPrExport } from '../tableDownload'; + +interface TableParams { + pagination?: TablePaginationConfig; + filterOpts?: FilterOptionInput[]; + sortOpts?: SortOptionInput; + filters?: Record; +} + +const MetricTable: React.FC<{ + label: string; + level: string; + beginDate: Date; + endDate: Date; + commonFilterOpts: any[]; +}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { + const { t } = useTranslation(); + const stateOption = useStateType(); + const [tableData, setData] = useState(); + const [tableParams, setTableParams] = useState({ + pagination: { + current: 1, + pageSize: 10, + showSizeChanger: true, + position: ['bottomCenter'], + showTotal: (total) => { + return `${t('analyze:total_prs', { total })} `; + }, + }, + filterOpts: [], + sortOpts: { + type: 'state', + direction: 'desc', + }, + }); + const query = { + page: tableParams.pagination.current, + per: tableParams.pagination.pageSize, + filterOpts: [...tableParams.filterOpts, ...commonFilterOpts], + sortOpts: tableParams.sortOpts, + label, + level, + beginDate, + endDate, + }; + const { isLoading, isFetching } = usePullsDetailListQuery(client, query, { + // enabled: false, + onSuccess: (data) => { + const items = data.pullsDetailList.items; + setData(items); + setTableParams({ + ...tableParams, + pagination: { + ...tableParams.pagination, + total: data.pullsDetailList.count, + }, + }); + }, + }); + const handleTableChange = ( + pagination: TablePaginationConfig, + filters: Record, + sorter: SorterResult + ) => { + let sortOpts = null; + let filterOpts = []; + sortOpts = sorter.field && { + type: toUnderline(sorter.field as string), + direction: sorter.order === 'ascend' ? 'asc' : 'desc', + }; + for (const key in filters) { + if (filters.hasOwnProperty(key)) { + const transformedObj = { + type: key, + values: filters[key] as string[], + }; + filters[key] && filterOpts.push(transformedObj); + } + } + setTableParams({ + pagination: { + showTotal: tableParams.pagination.showTotal, + ...pagination, + }, + sortOpts, + filterOpts, + }); + }; + + const columns: ColumnsType = [ + { + title: t('analyze:metric_detail:pr_title'), + dataIndex: 'title', + align: 'left', + width: '200px', + sorter: true, + fixed: 'left', + }, + { + title: 'URL', + dataIndex: 'url', + align: 'left', + width: '220px', + }, + { + title: t('analyze:metric_detail:state'), + dataIndex: 'state', + align: 'left', + width: '100px', + filters: stateOption, + sorter: true, + render: (text) => { + return stateOption.find((i) => i.value === text)?.text || text; + }, + }, + { + title: t('analyze:metric_detail:created_time'), + dataIndex: 'createdAt', + align: 'left', + width: '140px', + sorter: true, + render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), + }, + { + title: t('analyze:metric_detail:close_time'), + dataIndex: 'closedAt', + align: 'left', + width: '120px', + sorter: true, + render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), + }, + { + title: t('analyze:metric_detail:processing_time'), + dataIndex: 'timeToCloseDays', + align: 'left', + width: '200px', + sorter: true, + }, + { + title: t('analyze:metric_detail:first_response_time'), + dataIndex: 'timeToFirstAttentionWithoutBot', + align: 'left', + width: '220px', + sorter: true, + }, + { + title: t('analyze:metric_detail:comments_count'), + dataIndex: 'numReviewComments', + align: 'left', + width: '160px', + sorter: true, + }, + { + title: t('analyze:metric_detail:tags'), + dataIndex: 'labels', + align: 'left', + width: '100px', + render: (list) => list?.join(', ') || '', + }, + { + title: t('analyze:metric_detail:creator'), + dataIndex: 'userLogin', + align: 'left', + width: '100px', + }, + { + title: t('analyze:metric_detail:reviewer'), + dataIndex: 'reviewersLogin', + align: 'left', + width: '100px', + render: (list) => list?.join(',') || '', + }, + { + title: t('analyze:metric_detail:merge_author'), + dataIndex: 'mergeAuthorLogin', + align: 'left', + width: '140px', + }, + ]; + return ( + <> +
+ +
+ + + ); +}; +export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx new file mode 100644 index 000000000..126bdbb95 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx @@ -0,0 +1,131 @@ +import React, { useState, useMemo } from 'react'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import useVerifyDateRange from '../useVerifyDateRange'; +import MetricTable from './PrTable'; +import PrCompletion from './PrCompletion'; +import PrComments from './PrComments'; +import { useTranslation } from 'next-i18next'; +import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; +import { useRouter } from 'next/router'; +import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; +import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; + +const MetricPr = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { handleQueryParams } = useHandleQueryParams(); + const { verifiedItems } = useLabelStatus(); + const { label, level } = verifiedItems[0]; + const queryCard = router.query?.card as string; + const [tab, setTab] = useState(queryCard || '1'); + const { timeStart, timeEnd } = useVerifyDateRange(); + const [repoList, setRepoList] = useState([]); + const commonFilterOpts = useMemo(() => { + let opts = []; + if (repoList.length > 0) { + opts.push({ type: 'repo_urls', values: repoList }); + } + return opts; + }, [repoList]); + let source; + switch (tab) { + case '1': { + source = ( + + ); + break; + } + case '2': { + source = ( + + ); + break; + } + case '3': { + source = ( + + ); + break; + } + default: { + source = ( + + ); + break; + } + } + return ( +
+ setRepoList(v)} + type={'pr'} + /> + { + setTab(v); + handleQueryParams({ card: v }); + }} + aria-label="Tabs where selection follows focus" + selectionFollowsFocus + > + + + + +
{source}
+
+ ); +}; + +export default MetricPr; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx new file mode 100644 index 000000000..e5d7e9395 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { BsThreeDots } from 'react-icons/bs'; +import Popper from '@mui/material/Popper'; +import { ClickAwayListener } from '@mui/base/ClickAwayListener'; +import classnames from 'classnames'; + +interface CardDropDownMenuProps { + showOrgModel?: boolean; + orgModel?: boolean; + onOrgModelChange?: (v: boolean) => void; + onlyIdentity?: boolean; + onOnlyIdentityChange?: (v: boolean) => void; + onlyOrg?: boolean; + onOnlyOrgChange?: (v: boolean) => void; +} + +const CardDropDownMenu = (props: CardDropDownMenuProps) => { + const { + showOrgModel = false, + orgModel = true, + onlyIdentity = false, + onlyOrg = false, + onOrgModelChange, + onOnlyIdentityChange, + onOnlyOrgChange, + } = props; + + const { t } = useTranslation(); + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setOpen((previousOpen) => !previousOpen); + }; + + const canBeOpen = open && Boolean(anchorEl); + const id = canBeOpen ? 'transition-popper' : undefined; + + const ReferenceNode = ( + <> + {showOrgModel && ( +
{ + onOrgModelChange?.(!orgModel); + }} + > + + {t('analyze:metric_detail:show_organization')} + +
+ )} +
{ + onOnlyIdentityChange?.(!onlyIdentity); + }} + > + + {t('analyze:metric_detail:only_manager_participant')} + +
+
{ + onOnlyOrgChange?.(!onlyOrg); + }} + > + + {t('analyze:metric_detail:only_individual_organization')} + +
+ + ); + + return ( + <> + { + if (!open) return; + setOpen(() => false); + }} + > +
+
handleClick(e)} + > + +
+ +
+ {ReferenceNode} +
+
+
+
+ + ); +}; + +export default CardDropDownMenu; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx new file mode 100644 index 000000000..9c1dc6e0b --- /dev/null +++ b/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import MyTab from '@common/components/Tab'; +import MetricContributor from './MetricContributor'; +import MetricIssue from './MetricIssue'; +import MetricPr from './MetricPr'; +import { AiOutlineLeftCircle } from 'react-icons/ai'; +import MerticDatePicker from '@modules/analyze/components/NavBar/MerticDatePicker'; +import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import useVerifyDetailRangeQuery from '@modules/analyze/hooks/useVerifyDetailRangeQuery'; +import LoadingAnalysis from '@modules/analyze/DataView/Status/LoadingAnalysis'; +import LabelItems from '@modules/analyze/components/NavBar/LabelItems'; +import { useRouter } from 'next/router'; +import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; +import { Select } from 'antd'; + +const VerifyMetricDetail = () => { + const { isLoading } = useVerifyDetailRangeQuery(); + if (isLoading) { + return ; + } + return ; +}; +const MetricDetail = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { handleQueryParams } = useHandleQueryParams(); + const slugs = router.query.slugs; + const queryTab = router.query?.tab as string; + const { isLoading, verifiedItems } = useLabelStatus(); + const [tab, setTab] = useState(queryTab || 'contributor'); + if (isLoading || verifiedItems.length > 1) { + return null; + } + + const tabOptions = [ + { + label: t('analyze:metric_detail:contributors_persona'), + value: 'contributor', + }, + { label: t('analyze:metric_detail:issues'), value: 'issue' }, + { label: t('analyze:metric_detail:pull_requests'), value: 'pr' }, + ]; + + let source; + switch (tab) { + case 'contributor': { + source = ; + break; + } + case 'issue': { + source = ; + break; + } + case 'pr': { + source = ; + break; + } + default: { + break; + } + } + return ( +
+
+
+ { + // const query = window.location.search; + router.push('/analyze/' + slugs); + }} + className="mr-4 cursor-pointer text-[#3f60ef]" + /> +
+ +
+ + {t('analyze:metric_detail:project_deep_dive_insight')} + +
+
+ { + setTab(v); + handleQueryParams({ tab: v }); + }} + /> +
+
+ { + setKeyword(v.target.value); + setConfirmItem(null); + }} + /> + +
+ {!confirmItem && throttledKeyword && ( +
+
+ { + setConfirmItem(item); + }} + /> +
+
+ )} +
+
+ +
{ + const tl = gsap.timeline(); + tl.to(q('#add-compare-btn'), { display: 'none', duration: 0 }); + tl.to(ref.current, { + width: 450, + duration: 0.15, + }); + tl.to(q('#search-input'), { display: 'flex', duration: 0 }); + tl.to(q('#search-input'), { left: 0, duration: 0.15 }); + + tl.eventCallback('onComplete', () => { + inputRef.current?.focus(); + }); + }} + > + +
{t('analyze:compare')}
+
+ + ); +}; + +export default AddInput; diff --git a/apps/web/src/modules/developer/components/CompareBar/ColorSwitcher.tsx b/apps/web/src/modules/developer/components/CompareBar/ColorSwitcher.tsx new file mode 100644 index 000000000..9f2ddc8d6 --- /dev/null +++ b/apps/web/src/modules/developer/components/CompareBar/ColorSwitcher.tsx @@ -0,0 +1,142 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { + ChartThemeState, + chartThemeState, + updateThemeColor, +} from '@modules/developer/store'; +import { useClickAway, useHoverDirty, useLocalStorage } from 'react-use'; +import { CgColorPicker } from 'react-icons/cg'; +import { DefaultIndex, getPalette, colors } from '@common/options/color'; +import CPTooltip from '@common/components/Tooltip'; +import { getNameSpace } from '@common/utils'; +import { useSnapshot } from 'valtio'; +import Popper from '@mui/material/Popper'; + +const getColor = (label: string, theme: DeepReadonly) => { + const current = theme.color.find((i) => i.label === label); + if (!current) return { palette: [] }; + + const { paletteIndex } = current; + const palette = getPalette(paletteIndex); + return palette[DefaultIndex]; +}; + +const SHOWED_PICKER_TOOLTIPS_KEY = 'showed-picker-tooltips'; + +const ColorSwitcher: React.FC<{ + label: string; + className?: string; + showPickGuideIcon?: boolean; + showGuideTips?: boolean; +}> = ({ label, showPickGuideIcon = false, showGuideTips = false }) => { + const colorPopoverRef = useRef(null); + const iconsRef = useRef(null); + const [popoverVisible, setPopoverVisible] = useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + + const [hasShowedGuide, setShowedGuideTooltips] = useLocalStorage( + SHOWED_PICKER_TOOLTIPS_KEY, + false + ); + + const theme = useSnapshot(chartThemeState); + const color = getColor(label, theme); + + useClickAway(colorPopoverRef, () => { + setPopoverVisible(false); + }); + + const isHover = useHoverDirty(iconsRef); + + const isOpen = useMemo(() => { + if (isHover && !popoverVisible) { + return true; + } + return !hasShowedGuide && showGuideTips; + }, [hasShowedGuide, showGuideTips, isHover, popoverVisible]); + + useEffect(() => { + if (isOpen && isHover) { + setShowedGuideTooltips(true); + } + }, [isOpen, isHover, setShowedGuideTooltips]); + + return ( +
+
+ +
{ + setAnchorEl(event.currentTarget); + setPopoverVisible(true); + }} + > +
+
+
+ {showPickGuideIcon && ( + + )} +
+ + {showPickGuideIcon && ( +
+ {getNameSpace(label)} +
+ )} +
+ +
+ {colors.map((c, index) => ( +
{ + updateThemeColor({ label, paletteIndex: index }); + setPopoverVisible(false); + }} + > +
+
+ ))} +
+ + {/* {popoverVisible && ( +
+ {colors.map((c, index) => ( +
{ + updateThemeColor({ label, paletteIndex: index }); + setPopoverVisible(false); + }} + > +
+
+ ))} +
+ )} */} +
+ ); +}; + +export default ColorSwitcher; diff --git a/apps/web/src/modules/developer/components/CompareBar/CompareItem.tsx b/apps/web/src/modules/developer/components/CompareBar/CompareItem.tsx new file mode 100644 index 000000000..5cd5f86d8 --- /dev/null +++ b/apps/web/src/modules/developer/components/CompareBar/CompareItem.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Level } from '@modules/developer/constant'; +import classnames from 'classnames'; +import { getLastPathSegment } from '@common/utils'; +import { compareIdsRemove } from '@common/utils/links'; +import { getRouteAsPath } from '@common/utils/url'; +import ColorSwitcher from '@modules/developer/components/CompareBar/ColorSwitcher'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import { useRouter } from 'next/router'; +import { AiOutlineClose } from 'react-icons/ai'; +import qs from 'query-string'; + +type CompareItemProps = { + label: string; + name: string; + level: Level; + shortCode: string; +}; + +const CloseIcons: React.FC = ({ shortCode }) => { + const router = useRouter(); + const { compareSlugs } = useCompareItems(); + return ( +
{ + const p = compareIdsRemove(compareSlugs, shortCode); + const searchResult = qs.parse(window.location.search) || {}; + if (p.indexOf('..') > -1) { + await router.push( + getRouteAsPath(router.route, { slugs: p, ...searchResult }) + ); + return; + } + await router.push( + getRouteAsPath(router.route, { slugs: p, ...searchResult }) + ); + }} + > + +
+ ); +}; + +const CompareItem: React.FC<{ + item: CompareItemProps; + showCloseIcon: boolean; + showColorSwitch: boolean; + showGuideTips?: boolean; + className?: string; +}> = (props) => { + const { + item, + showColorSwitch, + showCloseIcon, + className, + showGuideTips = false, + } = props; + return ( +
+ {showCloseIcon && } +
+ {getLastPathSegment(item.label)} +
+ {showColorSwitch && ( + + )} +
+ ); +}; + +export default CompareItem; diff --git a/apps/web/src/modules/developer/components/CompareBar/SearchDropdown.tsx b/apps/web/src/modules/developer/components/CompareBar/SearchDropdown.tsx new file mode 100644 index 000000000..c9301670b --- /dev/null +++ b/apps/web/src/modules/developer/components/CompareBar/SearchDropdown.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import classnames from 'classnames'; +import Empty from '@common/components/Empty'; +import useDropDown from '@common/hooks/useDropDown'; +import { SearchQuery } from '@oss-compass/graphql'; +import { removeHttps } from '@common/utils'; +import CollectionTag from '@common/components/CollectionTag'; + +const DropDownList: React.FC<{ + result: SearchQuery['fuzzySearch']; + onConfirm: (item: SearchQuery['fuzzySearch'][number]) => void; +}> = ({ result, onConfirm }) => { + const { active } = useDropDown({ + totalLength: result.length, + onPressEnter: () => { + const cp = result[active]; + onConfirm(cp); + }, + }); + + return ( + <> + {result.map((item, index) => { + return ( +
{ + onConfirm(item); + }} + className="flex w-max" + > + + {removeHttps(item.label!)} + + +
+ ); + })} + + ); +}; + +const SearchDropdown: React.FC<{ + result: SearchQuery['fuzzySearch']; + onConfirm: (item: SearchQuery['fuzzySearch'][number]) => void; +}> = ({ result, onConfirm }) => { + if (!result) return ; + if (Array.isArray(result) && result.length === 0) { + return ; + } + + return ; +}; + +export default SearchDropdown; diff --git a/apps/web/src/modules/developer/components/CompareBar/index.tsx b/apps/web/src/modules/developer/components/CompareBar/index.tsx new file mode 100644 index 000000000..bf4086d54 --- /dev/null +++ b/apps/web/src/modules/developer/components/CompareBar/index.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import classnames from 'classnames'; +import AddInput from './AddInput'; +import useBreakpoint from '@common/hooks/useBreakpoint'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import CompareItem from './CompareItem'; + +const CompareBar: React.FC<{ lab?: boolean }> = ({ lab = false }) => { + const breakpoint = useBreakpoint(); + const { compareItems } = useCompareItems(); + const len = compareItems.length; + + return ( +
+ {lab && ( +
+ Lab +
+ )} +
+
+ {compareItems.map((item, index) => { + return ( + 1} + showColorSwitch={len > 1} + showGuideTips={index === 0 && breakpoint === 'lg'} + className={classnames({ + 'rounded-tl-lg rounded-bl-lg !border-l-0': index === 0, + '!pt-6': lab, + // 'text-center': len == 1, + })} + /> + ); + })} +
+
+ +
+ ); +}; + +export default withErrorBoundary(CompareBar, { + FallbackComponent: ErrorFallback, + onError(error, info) { + console.log(error, info); + // Do something with the error + // E.g. log to an error logging client here + }, +}); diff --git a/apps/web/src/modules/developer/components/ConnectLine.tsx b/apps/web/src/modules/developer/components/ConnectLine.tsx new file mode 100644 index 000000000..835db619e --- /dev/null +++ b/apps/web/src/modules/developer/components/ConnectLine.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const ConnectLine = () => { + return ( +
+ ); +}; + +export default ConnectLine; diff --git a/apps/web/src/modules/developer/components/Container/AnalyzeContainer.tsx b/apps/web/src/modules/developer/components/Container/AnalyzeContainer.tsx new file mode 100644 index 000000000..831c7b0b6 --- /dev/null +++ b/apps/web/src/modules/developer/components/Container/AnalyzeContainer.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren, useEffect } from 'react'; +import useLabelStatus from '@modules/developer/hooks/useLabelStatus'; +import { StatusContextProvider } from '@modules/developer/context'; +import PageInfoInit from '@modules/developer/components/PageInfoInit'; +import NoSsr from '@common/components/NoSsr'; + +const AnalyzeContainer: React.FC = ({ children }) => { + const { status, isLoading, notFound, verifiedItems } = useLabelStatus(); + + return ( + + + {children} + + + ); +}; + +export default AnalyzeContainer; diff --git a/apps/web/src/modules/developer/components/Container/ChartOptionContainer.tsx b/apps/web/src/modules/developer/components/Container/ChartOptionContainer.tsx new file mode 100644 index 000000000..1b59f2189 --- /dev/null +++ b/apps/web/src/modules/developer/components/Container/ChartOptionContainer.tsx @@ -0,0 +1,71 @@ +import React, { ReactNode } from 'react'; +import { EChartsOption } from 'echarts'; +import { formatISO } from '@common/utils'; +import { useSnapshot } from 'valtio'; +import { ChartThemeState, chartThemeState } from '@modules/developer/store'; +import { DataContainerResult } from '@modules/developer/type'; +import { DebugLogger } from '@common/debug'; + +const logger = new DebugLogger('ChartOptionContainer'); + +/** + * @deprecated use ChartOptionProvider instead + */ +const ChartOptionContainer = (props: { + data: DataContainerResult; + optionCallback: ( + input: DataContainerResult, + theme?: DeepReadonly + ) => EChartsOption; + indicators?: string; + children: ((args: { option: EChartsOption }) => ReactNode) | ReactNode; + _tracing?: string; +}) => { + const { optionCallback, indicators, children, data, _tracing } = props; + + if (_tracing) { + logger.debug(_tracing); + } + + const theme = useSnapshot(chartThemeState); + const { + isCompare, + compareLabels, + xAxis, + yResults, + summaryMean, + summaryMedian, + } = data; + + const echartsOpts = optionCallback( + { + isCompare, + compareLabels, + xAxis: xAxis.map((i) => formatISO(i)), + yResults, + summaryMean, + summaryMedian, + }, + theme + ); + + if (!isCompare) { + echartsOpts.grid = { + ...echartsOpts.grid, + top: indicators ? 50 : 10, + }; + echartsOpts.legend = { + show: false, + }; + } + + return ( + <> + {typeof children === 'function' + ? children({ option: echartsOpts }) + : children} + + ); +}; + +export default ChartOptionContainer; diff --git a/apps/web/src/modules/developer/components/Container/LegacyLabelRedirect.tsx b/apps/web/src/modules/developer/components/Container/LegacyLabelRedirect.tsx new file mode 100644 index 000000000..0126c7d61 --- /dev/null +++ b/apps/web/src/modules/developer/components/Container/LegacyLabelRedirect.tsx @@ -0,0 +1,72 @@ +import React, { PropsWithChildren } from 'react'; +import client from '@common/gqlClient'; +import { useRouter } from 'next/router'; +import { StatusVerifyQuery, useStatusVerifyQuery } from '@oss-compass/graphql'; +import { useQueries, useQueryClient } from '@tanstack/react-query'; +import useExtractUrlLabels from '@modules/developer/hooks/useExtractUrlLabels'; +import { + getShortAnalyzeLink, + getShortCompareLink, + getShortLabAnalyzeLink, + getShortLabCompareLink, +} from '@common//utils/links'; + +const LegacyLabelRedirect: React.FC< + PropsWithChildren & { isLab?: boolean } +> = ({ children, isLab = false }) => { + const router = useRouter(); + const queryClient = useQueryClient(); + const { urlLabels } = useExtractUrlLabels(); + + const queries = useQueries({ + queries: urlLabels.map(({ label }) => { + return { + queryKey: useStatusVerifyQuery.getKey({ label }), + queryFn: useStatusVerifyQuery.fetcher(client, { label }), + }; + }), + }); + + const isLoading = queries.some((query) => query.isLoading); + + const queriesResult = urlLabels.map(({ label }) => { + const key = useStatusVerifyQuery.getKey({ label }); + const data = queryClient.getQueryData(key); + return { ...data?.analysisStatusVerify }; + }); + + // server verified Items + const verifiedItems = queriesResult.filter( + (item) => Boolean(item?.label) && Boolean(item?.shortCode) + ); + + if (isLoading) { + return null; + } + + if (!isLoading && verifiedItems && verifiedItems.length > 0) { + // compare + if (verifiedItems.length > 1) { + const ids = verifiedItems.map((i) => i.shortCode!); + if (isLab) { + router.push(getShortLabCompareLink(ids)); + } else { + router.push(getShortCompareLink(ids)); + } + return null; + } + + // single + const id = verifiedItems[0].shortCode; + if (isLab) { + router.push(getShortLabAnalyzeLink(id)); + } else { + router.push(getShortAnalyzeLink(id)); + } + return null; + } + + return <>{children}; +}; + +export default LegacyLabelRedirect; diff --git a/apps/web/src/modules/developer/components/DefaultCardHead.tsx b/apps/web/src/modules/developer/components/DefaultCardHead.tsx new file mode 100644 index 000000000..65af0c84e --- /dev/null +++ b/apps/web/src/modules/developer/components/DefaultCardHead.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { BiExitFullscreen, BiFullscreen } from 'react-icons/bi'; + +export interface CardHeadProps { + id?: string; + title?: string; + description?: string; + fullScreen: boolean; + onFullScreen: (v: boolean) => void; +} + +const DefaultCardHead: React.FC = ({ + id, + title, + description, + onFullScreen, + fullScreen, +}) => { + return ( + <> +

+ {title} + + + # + + +

+ { + onFullScreen(!fullScreen); + }} + > + {fullScreen ? : } + +

{description}

+ + ); +}; + +export default DefaultCardHead; diff --git a/apps/web/src/modules/developer/components/DistributionMap/EChartGlOpt.ts b/apps/web/src/modules/developer/components/DistributionMap/EChartGlOpt.ts new file mode 100644 index 000000000..a05c1449c --- /dev/null +++ b/apps/web/src/modules/developer/components/DistributionMap/EChartGlOpt.ts @@ -0,0 +1,415 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; +import useQueryMetricType from '@modules/developer/hooks/useQueryMetricType'; +import { useMetricModelsOverviewQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import useLabelStatus from '@modules/developer/hooks/useLabelStatus'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import { Level } from '@modules/developer/constant'; + +export const useEchartsGlOpts = () => { + const { t } = useTranslation(); + const topicType = useQueryMetricType(); + const isContributor = topicType === 'contributor'; + const { verifiedItems } = useLabelStatus(); + const { label, level } = verifiedItems[0]; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { data, isLoading } = useMetricModelsOverviewQuery(client, { + label: label, + level: level, + repoType: level === Level.COMMUNITY ? repoType : null, + }); + const type = [ + t('analyze:collaboration'), + t('analyze:collaboration'), + t('analyze:collaboration'), + t('analyze:contributor'), + t('analyze:contributor'), + t('analyze:contributor'), + t('analyze:software'), + t('analyze:software'), + t('analyze:software'), + ]; + const contributorType = [ + t('analyze:contributor'), + t('analyze:contributor'), + t('analyze:contributor'), + t('analyze:collaboration'), + t('analyze:collaboration'), + t('analyze:collaboration'), + t('analyze:software'), + t('analyze:software'), + t('analyze:software'), + ]; + const dimension = [ + t('analyze:topic:productivity'), + t('analyze:topic:productivity'), + t('analyze:topic:productivity'), + t('analyze:topic:niche_creation'), + t('analyze:topic:niche_creation'), + t('analyze:topic:niche_creation'), + t('analyze:topic:robustness'), + t('analyze:topic:robustness'), + t('analyze:topic:robustness'), + ]; + const color = ['#93AAFC', '#87D8F8', '#B193FC']; + const disableColor = '#d4d4d4'; + const areaColor = [ + 'rgba(255,247,207,0.5)', + 'rgba(255,231,231,0.5)', + 'rgba(226,226,226,0.5)', + ]; + const contributorAreaColor = [ + 'rgba(255,231,231,0.5)', + 'rgba(255,247,207,0.5)', + 'rgba(226,226,226,0.5)', + ]; + const models = [ + { + name: t('analyze:all_model:collaboration_development_index'), + key: 'collab_dev_index', + value: [0, 1, 0], + itemStyle: { color: color[0] }, + disable: false, + scope: 'collaboration', + }, + { + name: t('analyze:all_model:community_service_and_support'), + key: 'community', + value: [2, 1, 0], + itemStyle: { color: color[0] }, + disable: false, + scope: 'collaboration', + }, + { + name: t('analyze:all_model:community_activity'), + key: 'activity', + value: [7, 1, 0], + itemStyle: { color: color[1] }, + disable: false, + scope: 'collaboration', + }, + { + name: t('analyze:all_model:organization_activity'), + key: 'organizations_activity', + value: [4, 1, 0], + itemStyle: { color: color[2] }, + disable: false, + scope: 'collaboration', + }, + { + name: t('analyze:all_model:contributors_domain_persona'), + key: 'domain_persona', + value: [0, 4, 0], + itemStyle: { color: color[0] }, + disable: false, + scope: 'contributor', + }, + { + name: t('analyze:all_model:contributors_role_persona'), + key: 'role_persona', + value: [1, 4, 0], + itemStyle: { color: color[0] }, + disable: false, + scope: 'contributor', + }, + { + name: t('analyze:all_model:contributors_milestone_persona'), + key: 'milestone_persona', + value: [2, 4, 0], + itemStyle: { color: color[0] }, + disable: false, + scope: 'contributor', + }, + { + name: t('analyze:all_model:contributor_route'), + key: 'contributor_route', + value: [4, 4, 30], + itemStyle: { color: color[1] }, + disable: true, + scope: 'contributor', + }, + { + name: t('analyze:all_model:contributor_reputation'), + key: 'contributor_reputation', + value: [6, 4, 30], + itemStyle: { color: color[2] }, + disable: true, + scope: 'contributor', + }, + { + name: t('analyze:all_model:user_reputation'), + key: 'user_reputation', + value: [8, 4, 40], + itemStyle: { color: color[2] }, + disable: true, + scope: 'contributor', + }, + { + name: t('analyze:all_model:software_quality'), + key: 'software_quality', + value: [0, 7, 50], + itemStyle: { color: color[0] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:software_usage_quality'), + key: 'software_usage_quality', + value: [1, 7, 40], + itemStyle: { color: color[0] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:document_quality'), + key: 'document_quality', + value: [2, 7, 60], + itemStyle: { color: color[0] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:northbound_adoption'), + key: 'northbound_adoption', + value: [3, 7, 30], + itemStyle: { color: color[1] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:south_fit'), + key: 'south_fit', + value: [5, 7, 30], + itemStyle: { color: color[1] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:security'), + key: 'security', + value: [6, 7, 60], + itemStyle: { color: color[2] }, + disable: true, + scope: 'software', + }, + { + name: t('analyze:all_model:compliance'), + key: 'compliance', + value: [8, 7, 40], + itemStyle: { color: color[2] }, + disable: true, + scope: 'software', + }, + ]; + const yAxis3D = isContributor ? contributorType : type; + const areaStyle = isContributor ? contributorAreaColor : areaColor; + const seriesData = models.map( + ({ key, value, itemStyle, scope, name, disable }) => { + if (isContributor) { + if (scope === 'collaboration') { + value = [value[0], value[1] + 3, value[2]]; + } else if (scope === 'contributor') { + value = [value[0], value[1] - 3, value[2]]; + } + } + const row = data?.metricModelsOverview.find((i) => i.ident === key); + if (row) { + value = [value[0], value[1], row.transformedScore]; + return { name, value, itemStyle }; + } else { + // value = [value[0], value[1], row.mainScore * 10]; + if (disable) { + itemStyle.color = disableColor; + return { name, value, itemStyle, disable: true }; + } + return { name, value, itemStyle, calc: true }; + } + } + ); + const echartsOpts = { + tooltip: { + textStyle: { + fontWeight: 'bolder', + }, + formatter: (params) => { + if (params.data.calc) { + return params.name + ': ' + t('analyze:statistics'); + } else if (params.data.disable) { + return params.name + ': ' + t('analyze:coming_soon'); + } else { + return params.name + ': ' + params.data.value[2]; + } + }, + }, + xAxis3D: { + name: ' ', + type: 'category', + data: dimension, + axisLine: { + lineStyle: { width: 1 }, + }, + axisLabel: { + interval: 2, + textStyle: { + fontSize: 10, + color: '#aaa', + }, + formatter: (index) => { + return ' ' + index; + }, + }, + splitLine: { + show: true, + interval: 2, + }, + axisTick: { + interval: 2, + length: 1, + }, + }, + zAxis3D: { + name: ' ', + type: 'value', + splitNumber: 4, + axisLine: { + lineStyle: { width: 0.1, opacity: 0.5 }, + }, + splitArea: { + show: true, + areaStyle: { + color: ['#ffffff', '#ffffff', '#ffffff'], + }, + }, + axisLabel: { + show: false, + }, + axisTick: { + show: false, + }, + }, + yAxis3D: { + name: ' ', + type: 'category', + data: yAxis3D, + axisLine: { + lineStyle: { width: 1 }, + }, + axisTick: { + interval: 2, + length: 1, + }, + splitArea: { + show: true, + areaStyle: { + color: areaStyle, + }, + }, + axisLabel: { + interval: 2, + textStyle: { + fontSize: 10, + color: '#aaa', + }, + }, + }, + series: [ + { + type: 'bar3D', + data: seriesData, + shading: 'realistic', + //金属质感,配合 ambientCubemap 使用 + // realisticMaterial: { + // roughness: 0.2, + // metalness: 1 + // }, + bevelSize: 0.1, + bevelSmoothness: 2, + barSize: 13, + label: { + show: false, + fontSize: 12, + fontWeight: 500, + borderWidth: 1, + color: '#333', + distance: -20, + // formatter: function (params) { + // var value = params.name; + // // 将超过指定字数的标签用 \n 换行 + // var maxLength = 2; // 指定字数 + // var rows = Math.ceil(value.length / maxLength); // 计算需要换几行 + // var result = '\n'; + // for (var i = 0; i < rows; i++) { + // var start = i * maxLength; + // var end = start + maxLength; + // result += value.substring(start, end) + '\n'; + // } + // return result; + // }, + }, + itemStyle: { + opacity: 0.95, + }, + //柱子高亮状态 + emphasis: { + label: { + show: false, + }, + itemStyle: { + color: '#FFB800', + opacity: 0.7, + }, + }, + }, + ], + grid3D: { + axisLine: { + interval: 1, + }, + //参考线 + axisPointer: { + //show: false, + lineStyle: { opacity: 0.2 }, + label: { show: false }, + }, + splitLine: { interval: 2 }, + //初始化摄像机视角 + viewControl: { + //透视与正交切换 perspective / orthographic + projection: 'perspective', + //自动旋转 + // autoRotate: true, + // autoRotateSpeed: 1, + //禁用缩放 + zoomSensitivity: 0, + rotateSensitivity: [2, 0], + distance: 290, + alpha: 20, + beta: 0, + }, + boxWidth: 200, + boxDepth: 150, + environment: 'none', + light: { + //主光源 + main: { + intensity: 0.8, + alpha: 50, + }, + //环境光源 + ambient: { + intensity: 0.5, + }, + // ambientCubemap: { + // texture: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/studio_small_08_1k.hdr', + // exposure: 0.6, + // diffuseIntensity: 0.5, + // specularIntensity: 1 + // } + }, + }, + }; + return { echartsOpts, isLoading }; +}; diff --git a/apps/web/src/modules/developer/components/DistributionMap/index.tsx b/apps/web/src/modules/developer/components/DistributionMap/index.tsx new file mode 100644 index 000000000..acca64bfb --- /dev/null +++ b/apps/web/src/modules/developer/components/DistributionMap/index.tsx @@ -0,0 +1,131 @@ +import React, { useMemo } from 'react'; +import BaseCard from '@common/components/BaseCard'; +import { useTranslation } from 'next-i18next'; +import classnames from 'classnames'; +import { BiFullscreen, BiExitFullscreen } from 'react-icons/bi'; +import type { EChartsOption } from 'echarts'; +import dynamic from 'next/dynamic'; +import Productivity from 'public/images/chart-legend/cube-1.svg'; +import Robustness from 'public/images/chart-legend/cube-2.svg'; +import NicheCreation from 'public/images/chart-legend/cube-3.svg'; +import { useEchartsGlOpts } from '@modules/developer/components/DistributionMap/EChartGlOpt'; +import Legend from '@common/components/EChartGl/Legend'; + +const EChartGl = dynamic(() => import('@common/components/EChartGl'), { + ssr: false, +}); +const DistributionMap = () => { + const { t } = useTranslation(); + const { echartsOpts, isLoading } = useEchartsGlOpts(); + return ( + ( + <> + { + setFullScreen(b); + }} + /> + + )} + > + {(containerRef, fullScreen) => + fullScreen ? ( +
+
+ +
+ +
+ ) : ( +
+
+ +
+ +
+ ) + } +
+ ); +}; +const MiniLegend = () => { + const { t } = useTranslation(); + + return ( +
+
+
+ + {t('analyze:topic:productivity')} +
+
+ + {t('analyze:topic:niche_creation')} +
+
+ + {t('analyze:topic:robustness')} +
+
+
+
+
+ {t('analyze:collaboration')} +
+
+
+ {t('analyze:contributor')} +
+
+
+ {t('analyze:software')} +
+
+
+ ); +}; +const FullScreen = (props) => { + const { t } = useTranslation(); + + return ( +
{ + props.onFullScreen(!props.fullScreen); + }} + > + {props.fullScreen ? ( + <> + + + {t('analyze:full_screen_exit')} + + + ) : ( + <> + + + {t('analyze:full_screen')} + + + )} +
+ ); +}; +export default DistributionMap; diff --git a/apps/web/src/modules/developer/components/DownCardLoadImage.tsx b/apps/web/src/modules/developer/components/DownCardLoadImage.tsx new file mode 100644 index 000000000..d7d357797 --- /dev/null +++ b/apps/web/src/modules/developer/components/DownCardLoadImage.tsx @@ -0,0 +1,287 @@ +import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import classnames from 'classnames'; +import { Portal } from '@mui/base/Portal'; +import qrcode from 'qrcode'; +import { getProvider, sleep } from '@common/utils'; +import { Level } from '@modules/developer/constant'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import ProviderIcon from '@modules/developer/components/ProviderIcon'; +import CompassSquareLogo from '@public/images/logos/compass-square.svg'; +import html2canvas from 'html2canvas'; +import { saveAs } from 'file-saver'; +import { elementToSVG, inlineResources } from 'dom-to-svg'; + +const genQrcode = (text: string): Promise => { + return new Promise((resolve, reject) => { + qrcode.toDataURL(text, { errorCorrectionLevel: 'H' }, (error, url) => { + if (error) reject(error); + return resolve(url); + }); + }); +}; + +interface DownLoadImageProps { + size: 'middle' | 'full'; + cardRef: RefObject; + loadingDownLoadImg: boolean; + qrcodeLink?: string; + fileFormat: string; + onComplete: () => void; + onCompleteLoad: () => void; +} + +const DownLoadImage = (props: DownLoadImageProps) => { + const { + cardRef, + size = 'middle', + loadingDownLoadImg, + fileFormat, + onComplete, + onCompleteLoad, + } = props; + + const { t } = useTranslation(); + const [qrcodeUrl, setQrcodeUrl] = useState(null); + const [dataURL, setDataUrl] = useState(null); + const downloadDivRef = useRef(null); + const downloadDivSvg = useRef(null); + + const { compareItems } = useCompareItems(); + + useEffect(() => { + const fetchData = async () => { + const qrcodeUrl = await genQrcode(window.location.href); + setQrcodeUrl(qrcodeUrl); + if (size === 'middle') { + cardRef.current!.style.width = '1200px'; + } + await sleep(300); + const canvas = await html2canvas(cardRef.current!, { + backgroundColor: 'rgba(0,0,0,0)', + ignoreElements: (el) => { + return el.hasAttribute('data-html2canvas-ignore'); + }, + }); + const dataURL = canvas.toDataURL('image/png'); + setDataUrl(dataURL); + cardRef.current!.style.removeProperty('width'); + await sleep(300); + onCompleteLoad(); + }; + const timer = setTimeout(() => { + fetchData().catch((e) => { + console.log('error:', e); + }); + }, 300); + return () => { + clearTimeout(timer); + }; + }, [cardRef, onCompleteLoad, size]); + + useEffect(() => { + const downLoadImg = async () => { + if (loadingDownLoadImg) { + // download img + if (fileFormat === 'PNG') { + const downloadImgCanvas = await html2canvas(downloadDivRef.current!, { + backgroundColor: 'rgba(0,0,0,0)', + }); + // trigger download + downloadImgCanvas.toBlob(function (blob) { + if (blob) { + saveAs(blob!, `${Date.now()}.png`); + } + onComplete(); + }); + } else { + // Capture specific element + const svgDocument = elementToSVG(downloadDivSvg.current!); + // Inline external resources (fonts, images, etc) as data: URIs + await inlineResources(svgDocument.documentElement); + // Get SVG string + const svgString = new XMLSerializer().serializeToString(svgDocument); + var link = document.createElement('a'); + link.download = `${Date.now()}.svg`; + link.href = + 'data:image/svg+xml;utf8,' + encodeURIComponent(svgString); + link.click(); + onComplete(); + } + } + }; + downLoadImg().catch((e) => { + console.log('error:', e); + }); + }, [loadingDownLoadImg, fileFormat, onComplete]); + + if (fileFormat === 'SVG') { + return ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + + {dataURL && ( +
+ Powered by oss-compass.org +
+ )} +
+
+ ); + } else { + return ( +
+
+ {compareItems.map(({ name, label, level }, index) => { + const host = getProvider(label); + + let labelNode = ( + + {name} + + ); + + if (level === Level.REPO) { + labelNode = ( + + {name} + + ); + } + return ( +
+ + {labelNode} + {level === Level.COMMUNITY && ( +
+ {t('home:community')} +
+ )} + {index < compareItems.length - 1 ? ( + vs + ) : null} +
+ ); + })} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+
+ +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+
+

+ Powered by oss-compass.org +

+

+ Scan the QR code above to read full report +

+
+ +
+
+ {compareItems.map(({ name, label, level }, index) => { + const host = getProvider(label); + + let labelNode = ( + + {name} + + ); + + if (level === Level.REPO) { + labelNode = ( + + {name} + + ); + } + return ( +
+ + {labelNode} + {level === Level.COMMUNITY && ( +
+ + {t('home:community')} + +
+ )} + {index < compareItems.length - 1 ? ( + vs + ) : null} +
+ ); + })} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+
+ +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+
+

+ Powered by oss-compass.org +

+

+ Scan the QR code above to read full report +

+
+
+
+
+ ); + } +}; + +export default DownLoadImage; diff --git a/apps/web/src/modules/developer/components/DownloadAndShare.tsx b/apps/web/src/modules/developer/components/DownloadAndShare.tsx new file mode 100644 index 000000000..1d4822941 --- /dev/null +++ b/apps/web/src/modules/developer/components/DownloadAndShare.tsx @@ -0,0 +1,327 @@ +import React, { useState, RefObject } from 'react'; +import cn from 'classnames'; +import { useRouter } from 'next/router'; +import { useCountDown } from 'ahooks'; +import { useTranslation } from 'next-i18next'; +import { PiShareFatLight } from 'react-icons/pi'; +import { GrClose } from 'react-icons/gr'; +import classnames from 'classnames'; +import Dialog from '@mui/material/Dialog'; +import { BiCopy } from 'react-icons/bi'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import { toast } from 'react-hot-toast'; +import { Transition } from '@common/components/Dialog'; +import Tooltip from '@common//components/Tooltip'; +import * as RadioGroup from '@radix-ui/react-radio-group'; +import DownCardLoadImage from './DownCardLoadImage'; +import { AiOutlineLoading } from 'react-icons/ai'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import useQueryDateRange from '@modules/developer/hooks/useQueryDateRange'; +import { rangeTags } from '../constant'; +import { format } from 'date-fns'; +import { toUnderline } from '@common/utils/format'; + +const queryMap = { + metricCodequality: 'collab_dev_index', + metricCommunity: 'community', + metricActivity: 'activity', + metricGroupActivity: 'organizations_activity', +}; + +const useGetSvgUrl = ( + slug: string, + id: string, + yAxisScale: boolean, + onePointSys: boolean, + yKey: string +) => { + const { range, timeStart, timeEnd } = useQueryDateRange(); + let url = `/chart/${slug}.svg`; + let metrc = ''; + let field = ''; + if (id === 'topic_overview') { + metrc = 'overview'; + } else { + const [metrcKey, fieldKey] = yKey.split('.'); + metrc = queryMap[metrcKey]; + if (id.indexOf('overview') === -1) { + field = toUnderline(fieldKey); + } + } + metrc && (url += `?metric=${metrc}`); + field && (url += `&field=${field}`); + !onePointSys && (url += `&y_trans=1`); + !yAxisScale && (url += `&y_abs=1`); + if ( + id === 'code_quality_is_maintained' || + id === 'code_quality_loc_frequency' + ) { + url += `&chart=bar`; + } + if (rangeTags.includes(range)) { + url += `&range=${range}`; + } else { + const begin_date = format(timeStart!, 'yyyy-MM-dd'); + const end_date = format(timeEnd!, 'yyyy-MM-dd'); + url += `&begin_date=${begin_date}&end_date=${end_date}`; + } + return url; +}; +const DownloadAndShare = (props: { + cardRef: RefObject; + downloadImageSize?: 'middle' | 'full'; + yAxisScale?: boolean; + onePointSys?: boolean; + yKey?: string; +}) => { + const { cardRef, downloadImageSize, yAxisScale, onePointSys, yKey } = props; + const { t } = useTranslation(); + const router = useRouter(); + const slug = router.query.slugs as string; + const { compareItems } = useCompareItems(); + const len = compareItems.length; + const svgUrl = useGetSvgUrl( + slug, + cardRef.current.id, + yAxisScale, + onePointSys, + yKey + ); + const [open, setOpen] = useState(false); + const [fileFormat, setFileFormat] = useState('SVG'); + const [loadingDownLoadImg, setLoadingDownLoadImg] = useState(false); + const [loadingPrviewImg, setLoadingPrviewImg] = useState(false); + + return ( + <> +
{ + setOpen(true); + }} + > + + + {t('analyze:download_chart_img')} + +
+ + { + setOpen(false); + }} + > +
+

+ {t('analyze:download_chart_img')} +

+
{ + setOpen(false); + }} + > + +
+
+ + {t('analyze:file_format')} + + { + setFileFormat(v); + }} + > +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ {loadingPrviewImg && ( +
{ + setLoadingDownLoadImg(true); + }} + className="absolute right-10 top-24 flex h-7 w-20 cursor-pointer items-center justify-center rounded-sm border border-[#3A5BEF] text-xs text-[#3A5BEF]" + > + {loadingDownLoadImg ? ( + + ) : ( + t('analyze:download') + )} +
+ )} +
+ { + setLoadingPrviewImg(true); + }} + onComplete={() => { + setLoadingDownLoadImg(false); + }} + /> + {fileFormat === 'SVG' + ? len === 1 && ( + + ) + : ''} +
+
+ + ); +}; + +const TabPanel = ({ badgeSrc, id }: { badgeSrc: string; id: string }) => { + const { t } = useTranslation(); + const [tab, setTab] = React.useState('Markdown'); + const [targetDate, setTargetDate] = useState(); + const [countdown] = useCountDown({ targetDate }); + const badgeLink = window.origin + badgeSrc; + const anchor = `${window.origin + window.location.pathname}#${id}`; + let source = ''; + switch (tab) { + case 'Markdown': { + source = `[![OSS Compass Analyze](${badgeLink})](${anchor})`; + break; + } + case 'HTML': { + source = `OSS Compass Analyze`; + break; + } + case 'Link': { + source = badgeLink; + break; + } + default: { + break; + } + } + + return ( + <> + { + setTab(v); + }} + aria-label="Tabs where selection follows focus" + selectionFollowsFocus + > + + + + +
+
{source}
+ +
{ + if (navigator.clipboard?.writeText) { + navigator.clipboard + .writeText(source) + .then((value) => { + setTargetDate(Date.now() + 800); + }) + .catch((err) => { + toast.error('Failed! No copy permission'); + }); + } else { + toast.error('Failed! Not Supported clipboard'); + } + }} + > + +
+
+
+ + ); +}; +export default DownloadAndShare; diff --git a/apps/web/src/modules/developer/components/HeaderWithFitlerBar.tsx b/apps/web/src/modules/developer/components/HeaderWithFitlerBar.tsx new file mode 100644 index 000000000..a4f77e1be --- /dev/null +++ b/apps/web/src/modules/developer/components/HeaderWithFitlerBar.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Header from '@common/components/Header'; +import StickyNav from '@common/components/Header/StickyNav'; +import { SideBarMenu } from '@modules/developer/components/SideBar'; +import NavBar from '@modules/developer/components/NavBar'; +import TopicNavbar from '@modules/developer/components/TopicNavbar'; + +const HeaderWithFilterBar = () => { + return ( + + {/* Head Black Including language switch, login */} +
+ +
+ } + /> + + {/* date picker, and parameter settings bar */} + + + + + ); +}; + +export default HeaderWithFilterBar; diff --git a/apps/web/src/modules/developer/components/MetricDetail/CommunityFilter.tsx b/apps/web/src/modules/developer/components/MetricDetail/CommunityFilter.tsx new file mode 100644 index 000000000..e9e220f31 --- /dev/null +++ b/apps/web/src/modules/developer/components/MetricDetail/CommunityFilter.tsx @@ -0,0 +1,93 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { Select, Checkbox, Divider } from 'antd'; +import { useCommunityReposSearchQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { getLastPathSegment } from '@common/utils'; +import { useTranslation } from 'next-i18next'; + +interface UserValue { + label: string; + value: string; +} + +const CommunityFilter = ({ label, onRepoChange }) => { + const [value, setValue] = useState([]); + const [options, setOptions] = useState([]); + const [selectState, setSelectState] = useState(true); + const { t } = useTranslation(); + const { isLoading } = useCommunityReposSearchQuery( + client, + { label: label, page: 1, per: 9999 }, + { + onSuccess: (data) => { + if (data) { + let items = data.communityRepos.items; + let opts = items.map((z) => ({ + label: getLastPathSegment(z.label), + value: z.label, + })); + setOptions(opts); + setValue(opts.map((item) => item.value)); + } + console.log(data); + }, + } + ); + + return ( + { + onBotChange(v); + // handleQueryParams({ tab: v }); + }} + value={isBot} + options={isBotOptions} + /> + + ) : ( + setContributor([e.target.value])} - onPressEnter={() => handleSearch()} - style={{ marginBottom: 8, display: 'block' }} - /> -
- - -
-
- ); -}; - -export default ContributorDropdown; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx deleted file mode 100644 index bb5ff4fb8..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/ContributorName.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { getIcons } from '../utils'; - -const DomainPersona = ({ name, origin }) => { - let icon = getIcons(origin, name); - let url = getHubUrl(origin, name); - return ( -
-
{icon}
- {url ? ( - - {name} - - ) : ( - name - )} -
- ); -}; - -const getHubUrl = (origin, name) => { - switch (origin) { - case 'github': - return 'https://github.com/' + name; - case 'gitee': - return 'https://gitee.com/' + name; - // return ; - default: - return null; - } -}; - -export default DomainPersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx deleted file mode 100644 index 17b1ca8f3..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/DomainPersona.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { useTranslation } from 'next-i18next'; -import { useGetContributionTypeI18n } from '../contribution'; -import { getDomainData, getIcons } from '../utils'; -import { toFixed } from '@common/utils'; -import classnames from 'classnames'; -import Popper from '@mui/material/Popper'; - -const PopperContent = ({ dataList, name, active, setActive, origin }) => { - const { t } = useTranslation(); - const activeItem = dataList - .find((item) => item.type === active) - ?.childern.sort((a, b) => b.contribution - a.contribution); - // const allType = ['Code', 'Code Admin', 'Issue', 'Issue Admin', 'Observe']; - return ( -
-
- {getIcons(origin, name)} - {name + ' ' + t('analyze:metric_detail:domain_persona_details')} -
-
-
- {dataList.map(({ type, color, contribution }) => { - return ( -
{ - setActive(type); - }} - className={classnames( - 'flex h-9 w-full cursor-pointer items-center justify-between border-b border-r bg-[#F6F6F6] last:border-b-0', - { '!border-r-0 !bg-[#FFFFFF]': active === type } - )} - > -
-
{type}
-
- {contribution} -
-
- ); - })} -
-
-
- {activeItem?.map(({ text, contribution }) => { - return ( -
-
{text}
-
{contribution}
-
- ); - })} -
-
-
- ); -}; - -const DomainPersona = ({ maxDomain, dataList, name, origin }) => { - const contributionTypeMap = useGetContributionTypeI18n(); - const domainData = useMemo(() => { - return getDomainData(dataList, contributionTypeMap); - }, [dataList, contributionTypeMap]); - const [active, setActive] = useState(''); - const [popperOpen, togglePopperOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - const handleClick = (event: React.MouseEvent, type) => { - setActive(type); - setAnchorEl(event.currentTarget); - togglePopperOpen(() => true); - }; - - return ( -
{ - setActive(''); - popperOpen && togglePopperOpen(() => false); - }} - > -
- {domainData.map(({ type, color, contribution }) => { - const width = toFixed((contribution / maxDomain) * 100, 2); - return active === type ? ( -
-
-
- ) : ( -
{ - handleClick(e, type); - }} - style={{ backgroundColor: color, width: `${width}%` }} - className="h-2 cursor-pointer" - >
- ); - })} -
- - - -
- ); -}; - -export default DomainPersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx deleted file mode 100644 index 04e06ddb0..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/RolePersona.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'next-i18next'; -import Tooltip from '@common/components/Tooltip'; -import { AiOutlineQuestionCircle } from 'react-icons/ai'; - -const RolePersona = () => { - const { t } = useTranslation(); - - return ( -
- {t('analyze:metric_detail:role_persona')} - {t('analyze:metric_detail:role_persona_desc')}} - placement="right" - > - - - - -
- ); -}; - -export default RolePersona; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx deleted file mode 100644 index ace9d46a8..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/ContributorTable/index.tsx +++ /dev/null @@ -1,412 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { - useContributorsDetailListQuery, - ContributorDetail, - FilterOptionInput, - SortOptionInput, -} from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import MyTable from '@common/components/Table'; -import classnames from 'classnames'; -import { - useContributionTypeLsit, - useEcologicalType, - useMileageOptions, -} from '../contribution'; -import { getMaxDomain } from '../utils'; -import { - getContributorPolling, - getContributorExport, -} from '../../tableDownload'; -import DomainPersona from './DomainPersona'; -import ContributorName from './ContributorName'; -import RolePersona from './RolePersona'; -import ContributorDropdown from './ContributorDropdown'; -import { useTranslation } from 'next-i18next'; -import Download from '@common/components/Table/Download'; -import { useRouter } from 'next/router'; -import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; -import Dialog from '@common/components/Dialog'; -import Tooltip from '@common/components/Tooltip'; -import ManageOrgEdit from '@common/components/OrgEdit/ManageOrgEdit'; -import useVerifyDetailRangeQuery from '@modules/analyze/hooks/useVerifyDetailRangeQuery'; -import { useIsCurrentUser } from '@modules/analyze/hooks/useIsCurrentUser'; -import { FiEdit } from 'react-icons/fi'; -import { GrClose } from 'react-icons/gr'; -import { AiOutlineSearch, AiFillFilter } from 'react-icons/ai'; -import getErrorMessage from '@common/utils/getErrorMessage'; -import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; -import type { FilterValue, SorterResult } from 'antd/es/table/interface'; -import toast from 'react-hot-toast'; -interface TableParams { - pagination?: TablePaginationConfig; - filterOpts?: FilterOptionInput[]; - sortOpts?: SortOptionInput; - filters?: Record; -} - -const MetricTable: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; - commonFilterOpts: any[]; -}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { - const { t } = useTranslation(); - const [openConfirm, setOpenConfirm] = useState(false); - const [currentName, setCurrentName] = useState(''); - const [currentOrgName, setCurrentOrgName] = useState(''); - const [origin, setOrigin] = useState(''); - - const { data } = useVerifyDetailRangeQuery(); - const { isCurrentUser } = useIsCurrentUser(); - const ecologicalOptions = useEcologicalType(); - const mileageOptions = useMileageOptions(); - const filterMap = { - ecologicalType: 'ecological_type', - contributionTypeList: 'contribution_type', - }; - const router = useRouter(); - const { handleQueryParams } = useHandleQueryParams(); - - const queryFilterOpts = router.query?.filterOpts as string; - const defaultFilterOpts = queryFilterOpts ? JSON.parse(queryFilterOpts) : []; - const defaultSortOpts = router.query?.sortOpts - ? JSON.parse(router.query?.sortOpts as string) - : null; - const [filterOpts, setFilterOpts] = useState(defaultFilterOpts || []); - const filterContributionType = useMemo(() => { - return filterOpts.find((i) => i.type === 'contribution_type'); - }, [filterOpts]); - const [tableData, setData] = useState(); - const [tableParams, setTableParams] = useState({ - pagination: { - current: 1, - pageSize: 10, - showSizeChanger: true, - position: ['bottomCenter'], - showTotal: (total) => { - return `${t('analyze:total_people', { total })} `; - }, - }, - sortOpts: defaultSortOpts, - }); - const query = { - page: tableParams.pagination.current, - per: tableParams.pagination.pageSize, - filterOpts: [...filterOpts, ...commonFilterOpts], - sortOpts: tableParams.sortOpts, - label, - level, - beginDate, - endDate, - }; - - const maxDomain = useMemo(() => { - return getMaxDomain(tableData); - }, [tableData]); - const { isLoading, isFetching } = useContributorsDetailListQuery( - client, - query, - { - onSuccess: (data) => { - const items = data.contributorsDetailList.items; - const hasTypeFilter = filterOpts.find( - (i) => i.type === 'contribution_type' - ); - if (hasTypeFilter) { - let value = hasTypeFilter.values; - items.map((item) => { - let list = item.contributionTypeList; - item.contributionTypeList = list.filter((i) => { - if (value.includes(i.contributionType)) { - return true; - } - }); - }); - } - setTableParams({ - ...tableParams, - pagination: { - ...tableParams.pagination, - total: data.contributorsDetailList.count, - }, - }); - setData(items); - setOrigin(data.contributorsDetailList.origin); - }, - onError: (e) => { - toast.error(getErrorMessage(e) || 'failed'); - }, - } - ); - const handleTableChange = ( - pagination: TablePaginationConfig, - filters: Record, - sorter: SorterResult - ) => { - let sortOpts = null; - let filterOpts = []; - for (const key in filters) { - if (filters.hasOwnProperty(key)) { - const transformedObj = { - type: filterMap[key] || key, - values: filters[key] as string[], - }; - filters[key] && filterOpts.push(transformedObj); - } - } - if (filterOpts.find((i) => i.type === 'contribution_type')) { - sortOpts = sorter.order && { - type: - sorter.field === 'contribution' - ? 'contribution_filterd' - : sorter.field, - direction: sorter.order === 'ascend' ? 'asc' : 'desc', - }; - } else { - sortOpts = sorter.order && { - type: sorter.field, - direction: sorter.order === 'ascend' ? 'asc' : 'desc', - }; - } - handleQueryParams({ - filterOpts: filterOpts.length > 0 ? JSON.stringify(filterOpts) : null, - sortOpts: sortOpts && JSON.stringify(sortOpts), - }); - setFilterOpts(filterOpts); - setTableParams({ - pagination: { - showTotal: tableParams.pagination.showTotal, - ...pagination, - }, - sortOpts, - }); - }; - - const columns: ColumnsType = [ - { - title: t('analyze:metric_detail:contributor'), - dataIndex: 'contributor', - align: 'left', - width: '200px', - fixed: 'left', - render: (name) => { - return ; - }, - filterIcon: (filtered: boolean) => ( - - ), - defaultFilteredValue: - defaultFilterOpts.find((i) => i.type === 'contributor')?.values || null, - filterDropdown: ({ selectedKeys, setSelectedKeys, confirm }) => { - return ( - - ); - }, - }, - { - title: , - dataIndex: 'ecologicalType', - align: 'left', - width: '200px', - filters: ecologicalOptions, - defaultFilteredValue: - defaultFilterOpts.find((i) => i.type === 'ecological_type')?.values || - null, - render: (text) => { - return ecologicalOptions.find((i) => i.value === text)?.text || text; - }, - }, - - { - title: t('analyze:metric_detail:milestone_persona'), - dataIndex: 'mileageType', - render: (text) => { - return mileageOptions.find((i) => i.value === text)?.label || text; - }, - align: 'left', - width: '200px', - }, - { - title: t('analyze:metric_detail:domain_persona'), - dataIndex: 'contributionTypeList', - render: (dataList, col) => { - return ( - - ); - }, - filters: useContributionTypeLsit(), - defaultFilteredValue: - defaultFilterOpts.find((i) => i.type === 'contribution_type')?.values || - null, - filterMode: 'tree', - align: 'left', - width: '300px', - }, - { - title: t('analyze:metric_detail:organization'), - dataIndex: 'organization', - align: 'left', - width: '160px', - render: (text, col) => { - let edit = null; - if (isCurrentUser(col.contributor)) { - edit = ( - { - window.open('/settings/profile'); - }} - /> - ); - } else if (data?.verifyDetailDataRange?.status) { - edit = ( - { - setCurrentName(col.contributor); - col.organization && setCurrentOrgName(col.organization); - setOpenConfirm(true); - }} - /> - ); - } else { - edit = ( - {t('analyze:no_role_desc')}
} - placement="top" - > -
- -
- - ); - } - return ( -
- {text || '-'} - {edit} -
- ); - }, - filterIcon: (filtered: boolean) => ( - - ), - defaultFilteredValue: - defaultFilterOpts.find((i) => i.type === 'organization')?.values || - null, - filterDropdown: ({ selectedKeys, setSelectedKeys, confirm }) => { - return ( - - ); - }, - }, - { - title: t('analyze:metric_detail:contribution'), - dataIndex: 'contribution', - key: 'contribution', - render: (contribution, record) => { - if (filterContributionType) { - let filterCount = record.contributionTypeList.reduce( - (total, obj) => total + obj.contribution, - 0 - ); - return filterCount; - } else { - return contribution; - } - }, - align: 'left', - width: '120px', - sorter: true, - }, - ]; - return ( - <> -
- -
- - -

- {currentName + - ' ' + - t('analyze:organization_information_modification')} -

-
{ - setOpenConfirm(false); - }} - > - -
- - } - dialogContent={ -
- { - setOpenConfirm(false); - }} - /> -
- } - handleClose={() => { - setOpenConfirm(false); - }} - /> - - ); -}; -export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx deleted file mode 100644 index e7544c81b..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/Contributors.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import React, { useRef, useMemo, useState } from 'react'; -import { useContributorsOverviewQuery } from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import { useTranslation } from 'next-i18next'; -import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; -import { gradientRamp } from '@common/options'; -import type { EChartsOption } from 'echarts'; -import { useGetEcologicalText } from './contribution'; -import PieDropDownMenu from '../PieDropDownMenu'; - -const getSeriesFun = (data, onlyIdentity, onlyOrg, getEcologicalText) => { - const legend = []; - const ecoData = []; - const contributorsData = []; - let allCount = 0; - - if (onlyIdentity || onlyOrg) { - if (data?.orgContributorsDistribution?.length > 0) { - const orgContributorsDistribution = data.orgContributorsDistribution; - const map = onlyIdentity - ? ['manager', 'participant'] - : ['individual', 'organization']; - - map.forEach((item, i) => { - let list = orgContributorsDistribution.filter((i) => - i?.subTypeName?.includes(item) - ); - let distribution = list.flatMap((i) => i.topContributorDistribution); - distribution.sort((a, b) => b.subCount - a.subCount); - const { name, index } = getEcologicalText(item); - const colorList = gradientRamp[index]; - let count = 0; - let otherCount = 0; - if (item === 'organization') { - distribution = distribution.reduce((acc, curr) => { - const found = acc.find((item) => item.subBelong === curr.subBelong); - if (found) { - found.subCount += curr.subCount; - } else { - acc.push({ - subBelong: curr.subBelong, - subName: curr.subBelong, - subCount: curr.subCount, - }); - } - return acc; - }, []); - } - - distribution.forEach((z, y) => { - const { subCount, subName } = z; - count += subCount; - if (subName == 'other' || y > 10) { - otherCount += subCount; - } else { - contributorsData.push({ - parentName: name, - name: subName, - value: subCount, - itemStyle: { color: colorList[y + 1] }, - }); - } - }); - otherCount && - contributorsData.push({ - parentName: name, - name: 'other', - value: otherCount, - itemStyle: { color: colorList[0] }, - }); - legend.push({ - index: index, - name: name, - itemStyle: { color: colorList[0] }, - }); - allCount += count; - ecoData.push({ - name: name, - value: count, - itemStyle: { color: colorList[0] }, - }); - }); - } - } else { - if (data?.orgContributorsDistribution?.length > 0) { - const orgContributorsDistribution = data.orgContributorsDistribution; - orgContributorsDistribution.forEach((item, i) => { - const { subTypeName, topContributorDistribution } = item; - const { name, index } = getEcologicalText(subTypeName); - const colorList = gradientRamp[index]; - let count = 0; - topContributorDistribution.forEach(({ subCount, subName }, index) => { - count += subCount; - contributorsData.push({ - parentName: name, - name: subName, - value: subCount, - itemStyle: { color: colorList[index + 1] }, - }); - }); - legend.push({ - name: name, - index: index, - itemStyle: { color: colorList[0] }, - }); - allCount += count; - ecoData.push({ - name: name, - value: count, - itemStyle: { color: colorList[0] }, - }); - }); - } - } - legend.sort((a, b) => a?.index - b?.index); - return { - legend, - allCount, - ecoData, - contributorsData, - }; -}; -const ContributorContributors: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; - commonFilterOpts: any[]; -}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { - const { t } = useTranslation(); - const getEcologicalText = useGetEcologicalText(); - const chartRef = useRef(null); - const [onlyIdentity, setOnlyIdentity] = useState(false); - const [onlyOrg, setOnlyOrg] = useState(false); - const { data, isLoading } = useContributorsOverviewQuery(client, { - label: label, - level: level, - beginDate: beginDate, - endDate: endDate, - // filterOpts: commonFilterOpts, - }); - - const getSeries = useMemo(() => { - return getSeriesFun(data, onlyIdentity, onlyOrg, getEcologicalText); - }, [data, onlyIdentity, onlyOrg, getEcologicalText]); - const unit: string = t('analyze:metric_detail:contributor_unit'); - const formatter = '{b} : {c}' + unit + ' ({d}%)'; - const option: EChartsOption = { - tooltip: { - trigger: 'item', - formatter: formatter, - }, - legend: { - top: 40, - left: 'center', - data: getSeries.legend, - }, - title: { - text: - t('analyze:metric_detail:contributor_distribution') + - '(' + - getSeries.allCount + - unit + - ')', - left: 'center', - }, - series: [ - { - top: 15, - name: '', - type: 'pie', - selectedMode: 'single', - radius: [0, '40%'], - label: { - position: 'inner', - fontSize: 12, - color: '#333', - formatter: formatter, - show: false, - }, - labelLine: { - show: false, - }, - labelLayout: { - hideOverlap: false, - moveOverlap: 'shiftY', - }, - data: getSeries.ecoData, - }, - { - top: 15, - name: '', - type: 'pie', - radius: ['50%', '62%'], - labelLine: { - length: 30, - }, - label: { - formatter: formatter, - color: '#333', - }, - data: getSeries.contributorsData, - }, - ], - }; - - return ( -
-
- { - setOnlyIdentity(b); - if (b) { - setOnlyOrg(false); - } - }} - onlyOrg={onlyOrg} - onOnlyOrgChange={(b) => { - setOnlyOrg(b); - if (b) { - setOnlyIdentity(false); - } - }} - /> -
- -
- ); -}; -export default ContributorContributors; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts deleted file mode 100644 index 9a98e1b95..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/contribution.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { useTranslation } from 'next-i18next'; - -// 领域画像 option -const useContributionTypeMap = () => { - const { t } = useTranslation(); - return { - Code: { - // code_author #code 编写 - // code_review #代码审查 - code_author: t('analyze:metric_detail:code:code_creation'), - code_review: t('analyze:metric_detail:code:code_review'), //delete - code_commit: t('analyze:metric_detail:code:code_commit'), //delete - pr_creation: t('analyze:metric_detail:code:pr_creation'), - pr_comments: t('analyze:metric_detail:code:pr_comments'), - }, - 'Code Admin': { - code_committer: t('analyze:metric_detail:code:code_committer'), - pr_labeled: t('analyze:metric_detail:code_admin:pr_labeled'), - pr_unlabeled: t('analyze:metric_detail:code_admin:pr_unlabeled'), - pr_closed: t('analyze:metric_detail:code_admin:pr_closed'), - pr_assigned: t('analyze:metric_detail:code_admin:pr_assigned'), - pr_unassigned: t('analyze:metric_detail:code_admin:pr_unassigned'), - pr_reopened: t('analyze:metric_detail:code_admin:pr_reopened'), - pr_milestoned: t('analyze:metric_detail:code_admin:pr_milestoned'), - pr_demilestoned: t('analyze:metric_detail:code_admin:pr_demilestoned'), - pr_marked_as_duplicate: t( - 'analyze:metric_detail:code_admin:pr_marked_as_duplicate' - ), - pr_transferred: t('analyze:metric_detail:code_admin:pr_transferred'), - pr_renamed_title: t('analyze:metric_detail:code_admin:pr_renamed_title'), - pr_change_description: t( - 'analyze:metric_detail:code_admin:pr_change_description' - ), - pr_setting_priority: t( - 'analyze:metric_detail:code_admin:pr_setting_priority' - ), - pr_change_priority: t( - 'analyze:metric_detail:code_admin:pr_change_priority' - ), - pr_merged: t('analyze:metric_detail:code_admin:pr_merged'), - pr_review: t('analyze:metric_detail:code_admin:pr_review'), - pr_set_tester: t('analyze:metric_detail:code_admin:pr_set_tester'), - pr_unset_tester: t('analyze:metric_detail:code_admin:pr_unset_tester'), - pr_check_pass: t('analyze:metric_detail:code_admin:pr_check_pass'), - pr_test_pass: t('analyze:metric_detail:code_admin:pr_test_pass'), - pr_reset_assign_result: t( - 'analyze:metric_detail:code_admin:pr_reset_assign_result' - ), - pr_reset_test_result: t( - 'analyze:metric_detail:code_admin:pr_reset_test_result' - ), - pr_link_issue: t('analyze:metric_detail:code_admin:pr_link_issue'), - pr_unlink_issue: t('analyze:metric_detail:code_admin:pr_unlink_issue'), - code_direct_commit: t( - 'analyze:metric_detail:code_admin:code_direct_commit' - ), //delete - }, - Issue: { - issue_creation: t('analyze:metric_detail:issue:issue_creation'), - issue_comments: t('analyze:metric_detail:issue:issue_comments'), - }, - 'Issue Admin': { - issue_labeled: t('analyze:metric_detail:issue_admin:issue_labeled'), - issue_unlabeled: t('analyze:metric_detail:issue_admin:issue_unlabeled'), - issue_closed: t('analyze:metric_detail:issue_admin:issue_closed'), - issue_reopened: t('analyze:metric_detail:issue_admin:issue_reopened'), - issue_assigned: t('analyze:metric_detail:issue_admin:issue_assigned'), - issue_unassigned: t('analyze:metric_detail:issue_admin:issue_unassigned'), - issue_milestoned: t('analyze:metric_detail:issue_admin:issue_milestoned'), - issue_demilestoned: t( - 'analyze:metric_detail:issue_admin:issue_demilestoned' - ), - issue_marked_as_duplicate: t( - 'analyze:metric_detail:issue_admin:issue_marked_as_duplicate' - ), - issue_transferred: t( - 'analyze:metric_detail:issue_admin:issue_transferred' - ), - issue_renamed_title: t( - 'analyze:metric_detail:issue_admin:issue_renamed_title' - ), - issue_change_description: t( - 'analyze:metric_detail:issue_admin:issue_change_description' - ), - issue_setting_priority: t( - 'analyze:metric_detail:issue_admin:issue_setting_priority' - ), - issue_change_priority: t( - 'analyze:metric_detail:issue_admin:issue_change_priority' - ), - issue_link_pull_request: t( - 'analyze:metric_detail:issue_admin:issue_link_pull_request' - ), - issue_unlink_pull_request: t( - 'analyze:metric_detail:issue_admin:issue_unlink_pull_request' - ), - issue_assign_collaborator: t( - 'analyze:metric_detail:issue_admin:issue_assign_collaborator' - ), - issue_unassign_collaborator: t( - 'analyze:metric_detail:issue_admin:issue_unassign_collaborator' - ), - issue_change_issue_state: t( - 'analyze:metric_detail:issue_admin:issue_change_issue_state' - ), - issue_change_issue_type: t( - 'analyze:metric_detail:issue_admin:issue_change_issue_type' - ), - issue_setting_branch: t( - 'analyze:metric_detail:issue_admin:issue_setting_branch' - ), - issue_change_branch: t( - 'analyze:metric_detail:issue_admin:issue_change_branch' - ), - }, - Observe: { - fork: t('analyze:metric_detail:observe:fork'), - star: t('analyze:metric_detail:observe:star'), - }, - }; -}; -// 领域画像 filter -export const useContributionTypeLsit = () => { - const obj = useContributionTypeMap(); - const result = []; - - for (const key in obj) { - const children = []; - for (const childKey in obj[key]) { - if ( - childKey !== 'code_commit' && - childKey !== 'code_direct_commit' && - childKey !== 'code_review' - ) { - children.push({ - text: obj[key][childKey], - value: childKey, - }); - } - } - result.push({ - text: key, - value: key, - children: children, - }); - } - return result; -}; -// 领域画像 i18n(表格字段翻译) -export const useGetContributionTypeI18n = () => { - const obj = useContributionTypeMap(); - const result = {}; - const colors = ['#4A90E2', '#9ECDF2', '#EAB308', '#FDE047', '#D1D5DB']; - const defaultColors = '#D1D5DB'; - function traverseObject(obj, color, type) { - for (const key in obj) { - if (typeof obj[key] === 'object') { - const c = colors.shift() || defaultColors; - traverseObject(obj[key], c, key); - } else { - result[key] = { text: obj[key], color, type }; - } - } - } - traverseObject(obj, defaultColors, null); - return result; -}; - -// 里程画像 i18n(表格字段翻译) -export const useMileageOptions = () => { - const { t } = useTranslation(); - - return [ - { label: t('analyze:metric_detail:core'), value: 'core' }, - { label: t('analyze:metric_detail:regular'), value: 'regular' }, - { label: t('analyze:metric_detail:guest'), value: 'guest' }, - ]; -}; - -//角色画像 option -export const useEcologicalType = () => { - const { t } = useTranslation(); - - return [ - { - text: t('analyze:metric_detail:organization_manager'), - value: 'organization manager', - }, - { - text: t('analyze:metric_detail:organization_participant'), - value: 'organization participant', - }, - { - text: t('analyze:metric_detail:individual_manager'), - value: 'individual manager', - }, - { - text: t('analyze:metric_detail:individual_participant'), - value: 'individual participant', - }, - ]; -}; -//角色画像 i18n -export const useGetEcologicalText = () => { - const { t } = useTranslation(); - const ecologicalOptions = useEcologicalType(); - const otherOptions = [ - { - text: t('analyze:metric_detail:organization'), - value: 'organization', - }, - { - text: t('analyze:metric_detail:individual'), - value: 'individual', - }, - - { - text: t('analyze:metric_detail:manager'), - value: 'manager', - }, - { - text: t('analyze:metric_detail:participant'), - value: 'participant', - }, - ]; - const options = [...ecologicalOptions, ...otherOptions]; - return (text) => { - const index = options.findIndex((i) => i.value === text); - return { - name: options[index]?.text || text, - index, - }; - }; -}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx deleted file mode 100644 index 0ceadc5ca..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/index.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import useVerifyDateRange from '../useVerifyDateRange'; -import { Checkbox } from 'antd'; -import { useTranslation } from 'next-i18next'; -import { useMileageOptions } from './contribution'; -import MetricTable from './ContributorTable'; -import ContributionCount from './ContributionCount'; -import ContributorContributors from './Contributors'; -import { AiOutlineQuestionCircle } from 'react-icons/ai'; -import Tooltip from '@common/components/Tooltip'; -import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; -import { useRouter } from 'next/router'; -import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; -import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; - -const MetricContributor = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { handleQueryParams } = useHandleQueryParams(); - const { verifiedItems } = useLabelStatus(); - const { label, level } = verifiedItems[0]; - const queryCard = router.query?.card as string; - const [tab, setTab] = useState(queryCard || '1'); - const { timeStart, timeEnd } = useVerifyDateRange(); - const options = useMileageOptions(); - const queryMileage = router.query?.mileage as string; - const defaultMileage = queryMileage - ? JSON.parse(queryMileage) - : ['core', 'regular']; - const [mileage, setMileage] = useState(defaultMileage); - const [isBot, setIsBot] = useState(false); - const [repoList, setRepoList] = useState([]); - - const onMileageChange = (checkedValues: string[]) => { - setMileage(checkedValues); - handleQueryParams({ mileage: JSON.stringify(checkedValues) }); - }; - const commonFilterOpts = useMemo(() => { - let opts = []; - if (mileage.length > 0) { - opts.push({ type: 'mileage_type', values: mileage }); - } - let botValues = ['false']; - if (isBot) { - botValues.push('true'); - } - opts.push({ type: 'is_bot', values: botValues }); - if (repoList.length > 0) { - opts.push({ type: 'repo_urls', values: repoList }); - } - return opts; - }, [mileage, isBot, repoList]); - let source; - switch (tab) { - case '1': { - source = ( - - ); - break; - } - case '2': { - source = ( - - ); - break; - } - case '3': { - source = ( - - ); - break; - } - default: { - source = ( - - ); - break; - } - } - return ( -
-
- setIsBot(v)} - onRepoChange={(v) => setRepoList(v)} - /> -
- - {t('analyze:metric_detail:milestone_persona_filter')} - -
- - {t('analyze:metric_detail:core')} : - - {t('analyze:metric_detail:core_desc')} -
-
- - {t('analyze:metric_detail:regular')} : - - {t('analyze:metric_detail:regular_desc')} -
-
- - {t('analyze:metric_detail:guest')} : - - {t('analyze:metric_detail:guest_desc')} -
- - } - placement="right" - > - - - -
-
- : - -
-
-
-
- { - setTab(v); - handleQueryParams({ card: v }); - }} - aria-label="Tabs where selection follows focus" - selectionFollowsFocus - > - - - - -
- -
{source}
-
-
- ); -}; - -export default MetricContributor; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx deleted file mode 100644 index 3d235f89d..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricContributor/utils.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { IoPeopleCircle } from 'react-icons/io5'; -import Image from 'next/image'; - -export const getMaxDomain = (tableData) => { - if (tableData?.length > 0) { - const filterData = tableData?.map((item) => { - let filterCount = item?.contributionTypeList?.reduce( - (acc, current) => acc + current.contribution, - 0 - ); - return { ...item, filterCount }; - }); - let maxCountElement = filterData?.reduce((prev, current) => - prev?.filterCount > current.filterCount ? prev : current - ); - return maxCountElement.filterCount; - } else { - return 0; - } -}; -export const getDomainData = (data, contributionTypeMap) => { - let arr = data.map((item) => { - return { ...item, ...contributionTypeMap[item.contributionType] }; - }); - const result = []; - arr.forEach(({ color, contribution, text, type }) => { - const domainType = result.find((z) => z.type === type); - if (domainType) { - domainType.contribution += contribution; - domainType.childern.push({ text, contribution }); - } else { - result.push({ - type, - color, - contribution, - childern: [{ text, contribution }], - }); - } - }); - return result.sort((a, b) => { - if (a.type < b.type) { - return -1; - } - if (a.type > b.type) { - return 1; - } - return 0; - }); -}; - -export const getIcons = (origin, name) => { - switch (origin) { - case 'github': - return ( -
- (e.currentTarget.src = '/images/github.png')} - unoptimized - fill={true} - style={{ - objectFit: 'cover', - }} - alt="icon" - placeholder="blur" - blurDataURL="/images/github.png" - /> -
- ); - case 'gitee': - return ( -
- - (e.currentTarget.src = '/images/logos/gitee-red.svg') - } - unoptimized - fill={true} - style={{ - objectFit: 'cover', - }} - alt="icon" - placeholder="blur" - blurDataURL="/images/logos/gitee-red.svg" - /> -
- ); - default: - return ; - } -}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx deleted file mode 100644 index e877f7e8f..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricDashboard.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'next-i18next'; -import { IoPeopleCircle, IoPersonCircle } from 'react-icons/io5'; -import { GoIssueOpened, GoGitPullRequestClosed } from 'react-icons/go'; -import { - AiFillClockCircle, - AiOutlineIssuesClose, - AiOutlineArrowRight, -} from 'react-icons/ai'; -import { BiChat, BiGitPullRequest, BiGitCommit } from 'react-icons/bi'; -import useCompareItems from '@modules/analyze/hooks/useCompareItems'; -import useQueryDateRange from '@modules/analyze/hooks/useQueryDateRange'; -import { - useMetricDashboardQuery, - ContributorDetailOverview, - IssueDetailOverview, - PullDetailOverview, -} from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import { SiGitee, SiGithub } from 'react-icons/si'; -import { toFixed } from '@common/utils'; -import { useRouter } from 'next/router'; -import Image from 'next/image'; - -const MetricDashboard = () => { - const { compareItems } = useCompareItems(); - const len = compareItems.length; - if (len > 1) { - return null; - } - return
; -}; - -const Main = () => { - const { t } = useTranslation(); - const { compareItems } = useCompareItems(); - const { timeStart, timeEnd } = useQueryDateRange(); - const router = useRouter(); - const slugs = router.query.slugs; - const { label, level } = compareItems[0]; - const { data, isLoading } = useMetricDashboardQuery(client, { - label: label, - level: level, - beginDate: timeStart, - endDate: timeEnd, - }); - - if (isLoading) { - return ( - <> -
-
- {t('analyze:metric_detail:project_deep_dive_insight')} -
-
{ - const query = window.location.search; - router.push('/analyze/insight/' + slugs + query); - }} - > - {t('analyze:metric_detail:details')} - -
-
- - - ); - } - return ( -
-
-
- {t('analyze:metric_detail:project_deep_dive_insight')} -
-
{ - const query = window.location.search; - router.push('/analyze/insight/' + slugs + query); - }} - > - {t('analyze:metric_detail:details')} - -
-
-
- -
- - -
-
-
- ); -}; - -const MetricBoxContributors: React.FC<{ - data: ContributorDetailOverview; -}> = ({ data }) => { - const { t } = useTranslation(); - return ( -
-
-
- {t('analyze:metric_detail:contributor')} -
-
-
-
-
-
- -
-
{data.contributorAllCount}
-
-
- {t('analyze:metric_detail:contributor_count')} -
-
-
-
- {getTopUser( - data.highestContributionContributor.origin, - data.highestContributionContributor.name - )} -
-
- {t('analyze:metric_detail:top_contributor')} -
-
-
-
-
- -
-
{data.orgAllCount}
-
-
- {t('analyze:metric_detail:org_count')} -
-
-
-
-
- {getIcons(data.highestContributionOrganization.origin)} -
-
- {data.highestContributionOrganization.name || '/'} -
-
-
- {t('analyze:metric_detail:top_contributing_org')} -
-
-
-
- ); -}; -const MetricBoxIssues: React.FC<{ - data: IssueDetailOverview; -}> = ({ data }) => { - const { t } = useTranslation(); - - return ( -
-
-
- {t('analyze:metric_detail:issues')} -
-
-
-
-
-
- -
-
{data.issueCount}
-
-
- {t('analyze:metric_detail:newly_created_issues')} -
-
-
-
-
- -
-
- {data.issueCompletionRatio - ? toFixed(data.issueCompletionRatio * 100, 1) + - '% (' + - data.issueCompletionCount + - ')' - : '/'} -
-
-
- {t('analyze:metric_detail:issue_completion_rate')} -
-
-
-
-
- -
-
{data.issueUnresponsiveCount}
-
-
- {t('analyze:metric_detail:unanswered_issue_count')} -
-
-
-
-
- -
-
- {toFixed(data.issueCommentFrequencyMean, 2)} -
-
-
- {t('analyze:metric_detail:average_comments_count')} -
-
-
-
- ); -}; - -const MetricBoxPr: React.FC<{ - data: PullDetailOverview; -}> = ({ data }) => { - const { t } = useTranslation(); - - return ( -
-
-
- {t('analyze:metric_detail:pull_requests')} -
-
-
-
-
-
- -
-
{data.pullCount}
-
-
- {t('analyze:metric_detail:newly_created_pr_count')} -
-
-
-
-
- -
-
- {data.pullCompletionRatio - ? toFixed(data.pullCompletionRatio * 100, 1) + - '% (' + - data.pullCompletionCount + - ')' - : '/'} -
-
-
- {t('analyze:metric_detail:pr_completion_rate')} -
-
-
-
-
- -
-
{data.pullUnresponsiveCount}
-
-
- {t('analyze:metric_detail:unanswered_pr_count')} -
-
-
-
-
- -
-
{data.commitCount}
-
-
- {t('analyze:metric_detail:commit_count')} -
-
-
-
- ); -}; -const Loading = () => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-); -const getIcons = (type: string) => { - switch (type) { - case 'github': - return ; - case 'gitee': - return ; - default: - return ; - } -}; -const getTopUser = (type, name) => { - let url = null; - let userIcon = null; - if (!name) { - userIcon = ; - } else { - switch (type) { - case 'github': - url = 'https://github.com/' + name; - userIcon = ( -
- (e.currentTarget.src = '/images/github.png')} - unoptimized - fill={true} - style={{ - objectFit: 'cover', - }} - alt="icon" - placeholder="blur" - blurDataURL="/images/github.png" - /> -
- ); - break; - case 'gitee': - url = 'https://gitee.com/' + name; - userIcon = ( -
- - (e.currentTarget.src = '/images/logos/gitee-red.svg') - } - unoptimized - fill={true} - alt="icon" - placeholder="blur" - blurDataURL="/images/logos/gitee-red.svg" - /> -
- ); - break; - default: - userIcon = ; - break; - } - } - - return ( - <> -
{userIcon}
-
- {url ? ( - - {name} - - ) : ( - name || '/' - )} -
- - ); -}; -export default MetricDashboard; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx deleted file mode 100644 index debeacff9..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueComments.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useRef, useMemo } from 'react'; -import { useIssueCommentQuery } from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import { useTranslation } from 'next-i18next'; -import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; -import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; - -const IssueCompletion: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; -}> = ({ label, level, beginDate, endDate }) => { - const { t } = useTranslation(); - const chartRef = useRef(null); - const { data, isLoading } = useIssueCommentQuery(client, { - label: label, - level: level, - beginDate: beginDate, - endDate: endDate, - }); - - const getSeries = useMemo(() => { - const distribution = data?.issuesDetailOverview?.issueCommentDistribution; - if (data && distribution?.length > 0) { - return distribution.map(({ subCount, subName }) => { - return { - name: subName + t('analyze:metric_detail:comments'), - value: subCount, - count: subCount, - }; - }); - } else { - return []; - } - }, [data, t]); - - const option = getPieOption({ seriesData: getSeries }); - - return ( -
- -
- ); -}; -export default IssueCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx deleted file mode 100644 index 2b0cdbaf9..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueCompletion.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useRef, useMemo } from 'react'; -import { useIssueCompletionQuery } from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import { useTranslation } from 'next-i18next'; -import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; -import type { EChartsOption } from 'echarts'; -import { useStateType } from './issue'; -import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; - -const IssueCompletion: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; -}> = ({ label, level, beginDate, endDate }) => { - const { t } = useTranslation(); - const stateOption = useStateType(); - const chartRef = useRef(null); - const { data, isLoading } = useIssueCompletionQuery(client, { - label: label, - level: level, - beginDate: beginDate, - endDate: endDate, - }); - const getStateText = (text) => { - return stateOption.find((i) => i.value === text)?.text || text; - }; - const getSeries = useMemo(() => { - const distribution = data?.issuesDetailOverview?.issueStateDistribution; - if (data && distribution?.length > 0) { - return distribution.map(({ subCount, subName }) => { - return { - name: getStateText(subName), - value: subCount, - }; - }); - } else { - return []; - } - }, [data, getStateText]); - - const option = getPieOption({ seriesData: getSeries }); - - return ( -
- -
- ); -}; -export default IssueCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx deleted file mode 100644 index 4ea9540c6..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/IssueTable.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useState } from 'react'; -import { - useIssuesDetailListQuery, - IssueDetail, - FilterOptionInput, - SortOptionInput, -} from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import MyTable from '@common/components/Table'; -import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; -import type { FilterValue, SorterResult } from 'antd/es/table/interface'; -import { useTranslation } from 'next-i18next'; -import { format, parseJSON } from 'date-fns'; -import { useStateType } from './issue'; -import { toUnderline } from '@common/utils/format'; -import Download from '@common/components/Table/Download'; -import { getIssuePolling, getIssueExport } from '../tableDownload'; - -interface TableParams { - pagination?: TablePaginationConfig; - filterOpts?: FilterOptionInput[]; - sortOpts?: SortOptionInput; - filters?: Record; -} - -const MetricTable: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; - commonFilterOpts: any[]; -}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { - const { t } = useTranslation(); - const stateOption = useStateType(); - const [tableData, setData] = useState(); - const [tableParams, setTableParams] = useState({ - pagination: { - current: 1, - pageSize: 10, - showSizeChanger: true, - position: ['bottomCenter'], - showTotal: (total) => { - return `${t('analyze:total_issues', { total })} `; - }, - }, - filterOpts: [], - sortOpts: { - type: 'state', - direction: 'desc', - }, - }); - - const query = { - page: tableParams.pagination.current, - per: tableParams.pagination.pageSize, - filterOpts: [...tableParams.filterOpts, ...commonFilterOpts], - sortOpts: tableParams.sortOpts, - label, - level, - beginDate, - endDate, - }; - const { isLoading, isFetching } = useIssuesDetailListQuery(client, query, { - // enabled: false, - onSuccess: (data) => { - const items = data.issuesDetailList.items; - setData(items); - setTableParams({ - ...tableParams, - pagination: { - ...tableParams.pagination, - total: data.issuesDetailList.count, - }, - }); - }, - }); - const handleTableChange = ( - pagination: TablePaginationConfig, - filters: Record, - sorter: SorterResult - ) => { - let sortOpts = null; - let filterOpts = []; - sortOpts = sorter.field && { - type: toUnderline(sorter.field as string), - direction: sorter.order === 'ascend' ? 'asc' : 'desc', - }; - for (const key in filters) { - if (filters.hasOwnProperty(key)) { - const transformedObj = { - type: key, - values: filters[key] as string[], - }; - filters[key] && filterOpts.push(transformedObj); - } - } - setTableParams({ - pagination: { - showTotal: tableParams.pagination.showTotal, - ...pagination, - }, - sortOpts, - filterOpts, - }); - }; - - const columns: ColumnsType = [ - { - title: t('analyze:metric_detail:issue_title'), - dataIndex: 'title', - align: 'left', - width: '200px', - sorter: true, - fixed: 'left', - }, - { - title: 'URL', - dataIndex: 'url', - align: 'left', - width: '220px', - }, - { - title: t('analyze:metric_detail:state'), - dataIndex: 'state', - align: 'left', - width: '100px', - sorter: true, - filters: stateOption, - render: (text) => { - return stateOption.find((i) => i.value === text)?.text || text; - }, - }, - { - title: t('analyze:metric_detail:created_time'), - dataIndex: 'createdAt', - align: 'left', - sorter: true, - width: '140px', - render: (time) => format(parseJSON(time)!, 'yyyy-MM-dd'), - }, - { - title: t('analyze:metric_detail:close_time'), - dataIndex: 'closedAt', - align: 'left', - sorter: true, - width: '120px', - render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), - }, - { - title: t('analyze:metric_detail:processing_time'), - dataIndex: 'timeToCloseDays', - align: 'left', - sorter: true, - width: '200px', - }, - { - title: t('analyze:metric_detail:first_response_time'), - dataIndex: 'timeToFirstAttentionWithoutBot', - align: 'left', - sorter: true, - width: '220px', - }, - { - title: t('analyze:metric_detail:comments_count'), - dataIndex: 'numOfCommentsWithoutBot', - align: 'left', - sorter: true, - width: '160px', - }, - { - title: t('analyze:metric_detail:tags'), - dataIndex: 'labels', - align: 'left', - render: (list) => list?.join(', ') || '', - width: '100px', - }, - { - title: t('analyze:metric_detail:creator'), - dataIndex: 'userLogin', - align: 'left', - width: '100px', - }, - { - title: t('analyze:metric_detail:assignee'), - dataIndex: 'assigneeLogin', - align: 'left', - width: '100px', - }, - ]; - return ( - <> -
- -
- - - ); -}; -export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx deleted file mode 100644 index bb3a5e9b4..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/index.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import useVerifyDateRange from '../useVerifyDateRange'; -import MetricTable from './IssueTable'; -import IssueCompletion from './IssueCompletion'; -import IssueComments from './IssueComments'; -import { useTranslation } from 'next-i18next'; -import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; -import { useRouter } from 'next/router'; -import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; -import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; - -const MetricIssue = () => { - const router = useRouter(); - const { handleQueryParams } = useHandleQueryParams(); - const { verifiedItems } = useLabelStatus(); - const { timeStart, timeEnd } = useVerifyDateRange(); - const { label, level } = verifiedItems[0]; - const { t } = useTranslation(); - const queryCard = router.query?.card as string; - const [repoList, setRepoList] = useState([]); - - const [tab, setTab] = useState(queryCard || '1'); - - const commonFilterOpts = useMemo(() => { - let opts = []; - if (repoList.length > 0) { - opts.push({ type: 'repo_urls', values: repoList }); - } - return opts; - }, [repoList]); - let source; - switch (tab) { - case '1': { - source = ( - - ); - break; - } - case '2': { - source = ( - - ); - break; - } - case '3': { - source = ( - - ); - break; - } - default: { - source = ( - - ); - break; - } - } - return ( -
- setRepoList(v)} - level={level} - label={label} - type={'issue'} - /> - { - setTab(v); - handleQueryParams({ card: v }); - }} - aria-label="Tabs where selection follows focus" - selectionFollowsFocus - > - - - - - -
{source}
-
- ); -}; - -export default MetricIssue; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts deleted file mode 100644 index 32cf630d3..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricIssue/issue.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -export const useStateType = () => { - const { t } = useTranslation(); - - return [ - { - text: t('analyze:metric_detail:open'), - value: 'open', - }, - { - text: t('analyze:metric_detail:closed'), - value: 'closed', - }, - { - text: t('analyze:metric_detail:progressing'), - value: 'progressing', - }, - { - text: t('analyze:metric_detail:rejected'), - value: 'rejected', - }, - ]; -}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts deleted file mode 100644 index edbbe5ba4..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PR.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -export const useStateType = () => { - const { t } = useTranslation(); - - return [ - { - text: t('analyze:metric_detail:open'), - value: 'open', - }, - { - text: t('analyze:metric_detail:closed'), - value: 'closed', - }, - { - text: t('analyze:metric_detail:merged'), - value: 'merged', - }, - ]; -}; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx deleted file mode 100644 index aa99d974c..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrComments.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useRef, useMemo } from 'react'; -import { usePullsCommentQuery } from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import { useTranslation } from 'next-i18next'; -import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; -import type { EChartsOption } from 'echarts'; -import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; - -const PrComments: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; -}> = ({ label, level, beginDate, endDate }) => { - const { t } = useTranslation(); - const chartRef = useRef(null); - const { data, isLoading } = usePullsCommentQuery(client, { - label: label, - level: level, - beginDate: beginDate, - endDate: endDate, - }); - const getSeries = useMemo(() => { - const distribution = data?.pullsDetailOverview?.pullCommentDistribution; - if (data && distribution?.length > 0) { - return distribution.map(({ subCount, subName }) => { - return { - name: subName + t('analyze:metric_detail:comments'), - value: subCount, - }; - }); - } else { - return []; - } - }, [data, t]); - - const option = getPieOption({ seriesData: getSeries }); - - return ( -
- -
- ); -}; -export default PrComments; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx deleted file mode 100644 index 313cd255b..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrCompletion.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useRef, useMemo } from 'react'; -import { usePullsCompletionQuery } from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import MetricChart from '@modules/analyze/DataView/MetricDetail/MetricChart'; -import { getPieOption } from '@modules/analyze/DataView/MetricDetail/metricChartOption'; -import { useStateType } from '@modules/analyze/DataView/MetricDetail/MetricPr/PR'; - -const PrCompletion: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; -}> = ({ label, level, beginDate, endDate }) => { - const stateOption = useStateType(); - const chartRef = useRef(null); - const { data, isLoading } = usePullsCompletionQuery(client, { - label: label, - level: level, - beginDate: beginDate, - endDate: endDate, - }); - const getStateText = (text) => { - return stateOption.find((i) => i.value === text)?.text || text; - }; - const getSeries = useMemo(() => { - const distribution = data?.pullsDetailOverview?.pullStateDistribution; - if (data && distribution?.length > 0) { - return distribution.map(({ subCount, subName }) => { - return { name: getStateText(subName), value: subCount }; - }); - } else { - return []; - } - }, [data, getStateText]); - - const option = getPieOption({ seriesData: getSeries }); - - return ( -
- -
- ); -}; -export default PrCompletion; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx deleted file mode 100644 index 47d027895..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/PrTable.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { useState } from 'react'; -import { - usePullsDetailListQuery, - PullDetail, - FilterOptionInput, - SortOptionInput, -} from '@oss-compass/graphql'; -import client from '@common/gqlClient'; -import MyTable from '@common/components/Table'; -import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; -import type { FilterValue, SorterResult } from 'antd/es/table/interface'; -import { useTranslation } from 'next-i18next'; -import { format, parseJSON } from 'date-fns'; -import { useStateType } from '@modules/analyze/DataView/MetricDetail/MetricPr/PR'; -import { toUnderline } from '@common/utils/format'; -import Download from '@common/components/Table/Download'; -import { getPrPolling, getPrExport } from '../tableDownload'; - -interface TableParams { - pagination?: TablePaginationConfig; - filterOpts?: FilterOptionInput[]; - sortOpts?: SortOptionInput; - filters?: Record; -} - -const MetricTable: React.FC<{ - label: string; - level: string; - beginDate: Date; - endDate: Date; - commonFilterOpts: any[]; -}> = ({ label, level, beginDate, endDate, commonFilterOpts }) => { - const { t } = useTranslation(); - const stateOption = useStateType(); - const [tableData, setData] = useState(); - const [tableParams, setTableParams] = useState({ - pagination: { - current: 1, - pageSize: 10, - showSizeChanger: true, - position: ['bottomCenter'], - showTotal: (total) => { - return `${t('analyze:total_prs', { total })} `; - }, - }, - filterOpts: [], - sortOpts: { - type: 'state', - direction: 'desc', - }, - }); - const query = { - page: tableParams.pagination.current, - per: tableParams.pagination.pageSize, - filterOpts: [...tableParams.filterOpts, ...commonFilterOpts], - sortOpts: tableParams.sortOpts, - label, - level, - beginDate, - endDate, - }; - const { isLoading, isFetching } = usePullsDetailListQuery(client, query, { - // enabled: false, - onSuccess: (data) => { - const items = data.pullsDetailList.items; - setData(items); - setTableParams({ - ...tableParams, - pagination: { - ...tableParams.pagination, - total: data.pullsDetailList.count, - }, - }); - }, - }); - const handleTableChange = ( - pagination: TablePaginationConfig, - filters: Record, - sorter: SorterResult - ) => { - let sortOpts = null; - let filterOpts = []; - sortOpts = sorter.field && { - type: toUnderline(sorter.field as string), - direction: sorter.order === 'ascend' ? 'asc' : 'desc', - }; - for (const key in filters) { - if (filters.hasOwnProperty(key)) { - const transformedObj = { - type: key, - values: filters[key] as string[], - }; - filters[key] && filterOpts.push(transformedObj); - } - } - setTableParams({ - pagination: { - showTotal: tableParams.pagination.showTotal, - ...pagination, - }, - sortOpts, - filterOpts, - }); - }; - - const columns: ColumnsType = [ - { - title: t('analyze:metric_detail:pr_title'), - dataIndex: 'title', - align: 'left', - width: '200px', - sorter: true, - fixed: 'left', - }, - { - title: 'URL', - dataIndex: 'url', - align: 'left', - width: '220px', - }, - { - title: t('analyze:metric_detail:state'), - dataIndex: 'state', - align: 'left', - width: '100px', - filters: stateOption, - sorter: true, - render: (text) => { - return stateOption.find((i) => i.value === text)?.text || text; - }, - }, - { - title: t('analyze:metric_detail:created_time'), - dataIndex: 'createdAt', - align: 'left', - width: '140px', - sorter: true, - render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), - }, - { - title: t('analyze:metric_detail:close_time'), - dataIndex: 'closedAt', - align: 'left', - width: '120px', - sorter: true, - render: (time) => (time ? format(parseJSON(time)!, 'yyyy-MM-dd') : ''), - }, - { - title: t('analyze:metric_detail:processing_time'), - dataIndex: 'timeToCloseDays', - align: 'left', - width: '200px', - sorter: true, - }, - { - title: t('analyze:metric_detail:first_response_time'), - dataIndex: 'timeToFirstAttentionWithoutBot', - align: 'left', - width: '220px', - sorter: true, - }, - { - title: t('analyze:metric_detail:comments_count'), - dataIndex: 'numReviewComments', - align: 'left', - width: '160px', - sorter: true, - }, - { - title: t('analyze:metric_detail:tags'), - dataIndex: 'labels', - align: 'left', - width: '100px', - render: (list) => list?.join(', ') || '', - }, - { - title: t('analyze:metric_detail:creator'), - dataIndex: 'userLogin', - align: 'left', - width: '100px', - }, - { - title: t('analyze:metric_detail:reviewer'), - dataIndex: 'reviewersLogin', - align: 'left', - width: '100px', - render: (list) => list?.join(',') || '', - }, - { - title: t('analyze:metric_detail:merge_author'), - dataIndex: 'mergeAuthorLogin', - align: 'left', - width: '140px', - }, - ]; - return ( - <> -
- -
- - - ); -}; -export default MetricTable; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx deleted file mode 100644 index 126bdbb95..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/MetricPr/index.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import useVerifyDateRange from '../useVerifyDateRange'; -import MetricTable from './PrTable'; -import PrCompletion from './PrCompletion'; -import PrComments from './PrComments'; -import { useTranslation } from 'next-i18next'; -import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; -import { useRouter } from 'next/router'; -import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; -import DetailHeaderFilter from '@modules/analyze/components/MetricDetail/DetailHeaderFilter'; - -const MetricPr = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { handleQueryParams } = useHandleQueryParams(); - const { verifiedItems } = useLabelStatus(); - const { label, level } = verifiedItems[0]; - const queryCard = router.query?.card as string; - const [tab, setTab] = useState(queryCard || '1'); - const { timeStart, timeEnd } = useVerifyDateRange(); - const [repoList, setRepoList] = useState([]); - const commonFilterOpts = useMemo(() => { - let opts = []; - if (repoList.length > 0) { - opts.push({ type: 'repo_urls', values: repoList }); - } - return opts; - }, [repoList]); - let source; - switch (tab) { - case '1': { - source = ( - - ); - break; - } - case '2': { - source = ( - - ); - break; - } - case '3': { - source = ( - - ); - break; - } - default: { - source = ( - - ); - break; - } - } - return ( -
- setRepoList(v)} - type={'pr'} - /> - { - setTab(v); - handleQueryParams({ card: v }); - }} - aria-label="Tabs where selection follows focus" - selectionFollowsFocus - > - - - - -
{source}
-
- ); -}; - -export default MetricPr; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx deleted file mode 100644 index e5d7e9395..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/PieDropDownMenu.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { BsThreeDots } from 'react-icons/bs'; -import Popper from '@mui/material/Popper'; -import { ClickAwayListener } from '@mui/base/ClickAwayListener'; -import classnames from 'classnames'; - -interface CardDropDownMenuProps { - showOrgModel?: boolean; - orgModel?: boolean; - onOrgModelChange?: (v: boolean) => void; - onlyIdentity?: boolean; - onOnlyIdentityChange?: (v: boolean) => void; - onlyOrg?: boolean; - onOnlyOrgChange?: (v: boolean) => void; -} - -const CardDropDownMenu = (props: CardDropDownMenuProps) => { - const { - showOrgModel = false, - orgModel = true, - onlyIdentity = false, - onlyOrg = false, - onOrgModelChange, - onOnlyIdentityChange, - onOnlyOrgChange, - } = props; - - const { t } = useTranslation(); - const [open, setOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - setOpen((previousOpen) => !previousOpen); - }; - - const canBeOpen = open && Boolean(anchorEl); - const id = canBeOpen ? 'transition-popper' : undefined; - - const ReferenceNode = ( - <> - {showOrgModel && ( -
{ - onOrgModelChange?.(!orgModel); - }} - > - - {t('analyze:metric_detail:show_organization')} - -
- )} -
{ - onOnlyIdentityChange?.(!onlyIdentity); - }} - > - - {t('analyze:metric_detail:only_manager_participant')} - -
-
{ - onOnlyOrgChange?.(!onlyOrg); - }} - > - - {t('analyze:metric_detail:only_individual_organization')} - -
- - ); - - return ( - <> - { - if (!open) return; - setOpen(() => false); - }} - > -
-
handleClick(e)} - > - -
- -
- {ReferenceNode} -
-
-
-
- - ); -}; - -export default CardDropDownMenu; diff --git a/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx b/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx deleted file mode 100644 index 9c1dc6e0b..000000000 --- a/apps/web/src/modules/developer/DataView/MetricDetail/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'next-i18next'; -import MyTab from '@common/components/Tab'; -import MetricContributor from './MetricContributor'; -import MetricIssue from './MetricIssue'; -import MetricPr from './MetricPr'; -import { AiOutlineLeftCircle } from 'react-icons/ai'; -import MerticDatePicker from '@modules/analyze/components/NavBar/MerticDatePicker'; -import useLabelStatus from '@modules/analyze/hooks/useLabelStatus'; -import { withErrorBoundary } from 'react-error-boundary'; -import ErrorFallback from '@common/components/ErrorFallback'; -import useVerifyDetailRangeQuery from '@modules/analyze/hooks/useVerifyDetailRangeQuery'; -import LoadingAnalysis from '@modules/analyze/DataView/Status/LoadingAnalysis'; -import LabelItems from '@modules/analyze/components/NavBar/LabelItems'; -import { useRouter } from 'next/router'; -import { useHandleQueryParams } from '@modules/analyze/hooks/useHandleQueryParams'; -import { Select } from 'antd'; - -const VerifyMetricDetail = () => { - const { isLoading } = useVerifyDetailRangeQuery(); - if (isLoading) { - return ; - } - return ; -}; -const MetricDetail = () => { - const { t } = useTranslation(); - const router = useRouter(); - const { handleQueryParams } = useHandleQueryParams(); - const slugs = router.query.slugs; - const queryTab = router.query?.tab as string; - const { isLoading, verifiedItems } = useLabelStatus(); - const [tab, setTab] = useState(queryTab || 'contributor'); - if (isLoading || verifiedItems.length > 1) { - return null; - } - - const tabOptions = [ - { - label: t('analyze:metric_detail:contributors_persona'), - value: 'contributor', - }, - { label: t('analyze:metric_detail:issues'), value: 'issue' }, - { label: t('analyze:metric_detail:pull_requests'), value: 'pr' }, - ]; - - let source; - switch (tab) { - case 'contributor': { - source = ; - break; - } - case 'issue': { - source = ; - break; - } - case 'pr': { - source = ; - break; - } - default: { - break; - } - } - return ( -
-
-
- { - // const query = window.location.search; - router.push('/analyze/' + slugs); - }} - className="mr-4 cursor-pointer text-[#3f60ef]" - /> -
- -
- - {t('analyze:metric_detail:project_deep_dive_insight')} - -
-
- { - setTab(v); - handleQueryParams({ tab: v }); - }} - /> -
-
-