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 000000000..2419cd624 Binary files /dev/null and b/apps/web/public/images/test/area-stack (1).png differ diff --git a/apps/web/public/images/test/area-stack (2).png b/apps/web/public/images/test/area-stack (2).png new file mode 100644 index 000000000..7ea5f6090 Binary files /dev/null and b/apps/web/public/images/test/area-stack (2).png differ diff --git a/apps/web/public/images/test/test.png b/apps/web/public/images/test/test.png new file mode 100644 index 000000000..0b228113e Binary files /dev/null and b/apps/web/public/images/test/test.png differ diff --git a/apps/web/public/images/test/test1.png b/apps/web/public/images/test/test1.png new file mode 100644 index 000000000..dd4962a7a Binary files /dev/null and b/apps/web/public/images/test/test1.png differ diff --git a/apps/web/public/images/test/test2.png b/apps/web/public/images/test/test2.png new file mode 100644 index 000000000..80f035881 Binary files /dev/null and b/apps/web/public/images/test/test2.png differ diff --git a/apps/web/public/images/test/test3.png b/apps/web/public/images/test/test3.png new file mode 100644 index 000000000..e7c686893 Binary files /dev/null and b/apps/web/public/images/test/test3.png differ diff --git a/apps/web/public/images/test/test4.png b/apps/web/public/images/test/test4.png new file mode 100644 index 000000000..088ff8ca4 Binary files /dev/null and b/apps/web/public/images/test/test4.png differ diff --git a/apps/web/public/images/test/test5.png b/apps/web/public/images/test/test5.png new file mode 100644 index 000000000..7539c729d Binary files /dev/null and b/apps/web/public/images/test/test5.png differ diff --git a/apps/web/public/images/test/test6.png b/apps/web/public/images/test/test6.png new file mode 100644 index 000000000..671d83e24 Binary files /dev/null and b/apps/web/public/images/test/test6.png differ diff --git a/apps/web/public/images/test/test7.png b/apps/web/public/images/test/test7.png new file mode 100644 index 000000000..75af9edc6 Binary files /dev/null and b/apps/web/public/images/test/test7.png differ diff --git a/apps/web/src/modules/developer/DataView/Charts.tsx b/apps/web/src/modules/developer/DataView/Charts.tsx new file mode 100644 index 000000000..ff6246a94 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Charts.tsx @@ -0,0 +1,98 @@ +import React, { PropsWithChildren, useEffect } from 'react'; +import { useTranslation } from 'next-i18next'; +import ChartsDataProvider from '../context/ChartsDataProvider'; +import OverviewSummary from './OverviewSummary'; +import RepoDataView from './RepoDataView'; +import CommunityServiceSupport from './CommunityServiceSupport'; +import CommunityActivity from './CommunityActivity'; +import OrganizationsActivity from './OrganizationsActivity'; +import TopicTitle from '@modules/developer/components/TopicTitle'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import { CiGrid41 } from 'react-icons/ci'; +import ProductivityIcon from '@modules/developer/components/SideBar/assets/Productivity.svg'; +import RobustnessIcon from '@modules/developer/components/SideBar/assets/Robustness.svg'; +import NicheCreationIcon from '@modules/developer/components/SideBar/assets/NicheCreation.svg'; + +const Charts = () => { + const { t } = useTranslation(); + + return ( + + {/*

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

*/} + +
+ ); +}; +const ChartsDataView = () => { + const { t } = useTranslation(); + return ( + <> + {/* } + id={Topic.Productivity} + > + {t('analyze:overview')} + */} +

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

+ + + } + id={Topic.Productivity} + > + 仓库 + + + + } + id={Topic.Robustness} + > + 协作 + + + + } + id={Topic.NicheCreation} + > + Code + + + + } + id={Topic.NicheCreation} + > + Issues + + + + ); +}; + +export default Charts; 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..ba0fb43d6 --- /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..faab3f727 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScore.tsx @@ -0,0 +1,86 @@ +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'; +import ImageFallback from '@common/components/ImageFallback'; + +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 ( + ( + <> + { + 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/TotalScoreRepo.tsx b/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScoreRepo.tsx new file mode 100644 index 000000000..31a9a408b --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/TotalScoreRepo.tsx @@ -0,0 +1,87 @@ +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 ChartWithData from '@modules/developer/components/ChartWithData'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import ImageFallback from '@common/components/ImageFallback'; + +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 ( + ( + <> + { + 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..121d0e028 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityActivity/index.tsx @@ -0,0 +1,56 @@ +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 ConnectLineMini from '@modules/developer/components/ConnectLineMini'; +import TotalScoreRepo from './TotalScoreRepo'; + +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..5ca34b330 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/CommentFrequency.tsx @@ -0,0 +1,81 @@ +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/TotalScoreRepo.tsx b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScoreRepo.tsx new file mode 100644 index 000000000..49b2cf5e5 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/TotalScoreRepo.tsx @@ -0,0 +1,87 @@ +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 ChartWithData from '@modules/developer/components/ChartWithData'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import ImageFallback from '@common/components/ImageFallback'; + +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 ( + ( + <> + { + 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..11352bbfe --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/UpdatedIssuesCount.tsx @@ -0,0 +1,82 @@ +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..dfbc082a7 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/CommunityServiceSupport/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import UpdatedIssuesCount from './UpdatedIssuesCount'; +import CommentFrequency from './CommentFrequency'; +import ConnectLineMini from '@modules/developer/components/ConnectLineMini'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; + +const CommunitySupport = () => { + const { t } = useTranslation(); + return ( + <> +
+ + + +
+ + ); +}; + +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/OrganizationsActivity/CodeMergeRatio.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/CodeMergeRatio.tsx new file mode 100644 index 000000000..921be8ef2 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/CodeMergeRatio.tsx @@ -0,0 +1,143 @@ +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: '创建PR', value: '1' }, + { label: '合并PR', value: '2' }, + ]; + + 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/OrganizationsActivity/CommitFrequency.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/CommitFrequency.tsx new file mode 100644 index 000000000..67decfa0d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/CommitFrequency.tsx @@ -0,0 +1,79 @@ +import React, { useMemo } from 'react'; +import { Organizations } 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 { TransOpt, GenChartOptions } from '@modules/developer/type'; +import { useTranslation } from 'next-i18next'; +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:organization_activity.metrics.commit_frequency' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricGroupActivity.commitFrequency', + summaryKey: 'summaryGroupActivity.commitFrequency', + }; + 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 CommitFrequency; diff --git a/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributionLast.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributionLast.tsx new file mode 100644 index 000000000..43635cc4d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributionLast.tsx @@ -0,0 +1,82 @@ +import React, { useMemo } from 'react'; +import { Organizations } 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 ContributionLast = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: 'contribution last', + xKey: 'grimoireCreationDate', + yKey: 'metricGroupActivity.contributionLast', + summaryKey: 'summaryGroupActivity.contributionLast', + }; + 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 ContributionLast; diff --git a/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributorCount.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributorCount.tsx new file mode 100644 index 000000000..ea5d90f13 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/ContributorCount.tsx @@ -0,0 +1,83 @@ +import React, { useMemo } from 'react'; +import { Organizations } 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:organization_activity.metrics.contributor_count' + ), + xKey: 'grimoireCreationDate', + yKey: 'metricGroupActivity.contributorCount', + summaryKey: 'summaryGroupActivity.contributorCount', + }; + 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 ContributorCount; diff --git a/apps/web/src/modules/developer/DataView/OrganizationsActivity/LocFrequency.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/LocFrequency.tsx new file mode 100644 index 000000000..6f79449c3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/LocFrequency.tsx @@ -0,0 +1,139 @@ +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/OrganizationsActivity/OrgCount.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/OrgCount.tsx new file mode 100644 index 000000000..c5b5a0e9f --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/OrgCount.tsx @@ -0,0 +1,79 @@ +import React, { useMemo } from 'react'; +import { Organizations } 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 { TransOpt, GenChartOptions } from '@modules/developer/type'; +import { useTranslation } from 'next-i18next'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; + +const OrgCount = () => { + const { t } = useTranslation(); + const tansOpts: TransOpt = { + legendName: 'org count', + xKey: 'grimoireCreationDate', + yKey: 'metricGroupActivity.orgCount', + summaryKey: 'summaryGroupActivity.orgCount', + }; + 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 OrgCount; diff --git a/apps/web/src/modules/developer/DataView/OrganizationsActivity/TotalScore.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/TotalScore.tsx new file mode 100644 index 000000000..6da598d99 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/TotalScore.tsx @@ -0,0 +1,83 @@ +import React, { useMemo, useState } from 'react'; +import { Organizations } 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:organizations_activity.title'), + xKey: 'grimoireCreationDate', + yKey: 'metricGroupActivity.organizationsActivity', + summaryKey: 'summaryGroupActivity.organizationsActivity', + }; + 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/OrganizationsActivity/index.tsx b/apps/web/src/modules/developer/DataView/OrganizationsActivity/index.tsx new file mode 100644 index 000000000..22dfad54a --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OrganizationsActivity/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import TotalScore from './TotalScore'; +import CommitFrequency from './CommitFrequency'; +import LocFrequency from './LocFrequency'; +import CodeMergeRatio from './CodeMergeRatio'; + +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import ConnectLineMini from '@modules/developer/components/ConnectLineMini'; + +const OrganizationsActivity = () => { + const { t } = useTranslation(); + + return ( + <> +
+ + + + + +
+ + {/*
+ + + + + +
*/} + + ); +}; + +export default withErrorBoundary(OrganizationsActivity, { + 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/OverviewSummary/Calendar/CalendarChart.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Calendar/CalendarChart.tsx new file mode 100644 index 000000000..0532e5a1d --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Calendar/CalendarChart.tsx @@ -0,0 +1,189 @@ +import React, { useMemo, useState } from 'react'; +import { ChartSummaryProps, getLineOption, line } from '@common/options'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import transHundredMarkSystem from '@common/transform/transHundredMarkSystem'; +import { transDataForOverview } from '@common/transform/transDataForOverview'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import ImageFallback from '@common/components/ImageFallback'; +import { TransOpt } from '@modules/developer/type'; +import Tab from '@common/components/Tab'; + +const CalendarChart: React.FC = ({ + loading = false, + xAxis, + yAxis, +}) => { + 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: '代码', value: '2' }, + { label: 'PR', value: '3' }, + { label: 'Issues', value: '4' }, + ]; + const [onePointSys, setOnePointSys] = useState( + chartUserSettingState.onePointSys + ); + const [yAxisScale, setYAxisScale] = useState( + chartUserSettingState.yAxisScale + ); + + const echartsOpts = useMemo(() => { + const series = yAxis.map(({ legendName, data }) => { + !onePointSys && (data = data.map((i) => transHundredMarkSystem(i))); + return line({ name: legendName, data }); + }); + return getLineOption({ + xAxisData: xAxis, + series, + yAxis: { + type: 'value', + scale: yAxisScale, + }, + }); + }, [xAxis, yAxis, yAxisScale, onePointSys]); + + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + yAxisScale={yAxisScale} + onYAxisScaleChange={(v) => { + setYAxisScale(v); + }} + onePointSys={onePointSys} + /> + + )} + > + {(containerRef) => ( + <> +
+ setTab(v as TabValue)} + /> +
+ + + + // + )} +
+ ); +}; + +const dateKey = 'grimoireCreationDate'; + +const CalendarChartWithData = () => { + const { t } = useTranslation(); + const opts = [ + { + type: 'metricCodequality', + key: 'codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + }, + { + type: 'metricCommunity', + key: 'communitySupportScore', + legendName: t('metrics_models:community_service_and_support.title'), + }, + { + type: 'metricActivity', + key: 'activityScore', + legendName: t('metrics_models:activity.title'), + }, + ]; + + const optsWithOrg = [ + ...opts, + { + type: 'metricGroupActivity', + key: 'organizationsActivity', + legendName: t('metrics_models:organization_activity.title'), + }, + ]; + + const data = useMetricQueryData(); + + const isLoading = data.loading; + const copyOpts = optsWithOrg; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { xAxis, yAxisResult } = useMemo(() => { + const result = data.items[0].result; + if (!result) return { xAxis: [], yAxisResult: [] }; + + return transDataForOverview(result, copyOpts, dateKey, repoType); + }, [copyOpts, data, repoType]); + + return ( + + ); +}; + +export default CalendarChartWithData; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Calendar/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Calendar/index.tsx new file mode 100644 index 000000000..9581f52bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Calendar/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import CalendarChart from './CalendarChart'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + <> +
+
+ +
+
+ + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ; +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/OverviewSummary/Cloud/CalendarChart.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Cloud/CalendarChart.tsx new file mode 100644 index 000000000..2cd918e46 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Cloud/CalendarChart.tsx @@ -0,0 +1,178 @@ +import React, { useMemo, useState } from 'react'; +import { ChartSummaryProps, getLineOption, line } from '@common/options'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import transHundredMarkSystem from '@common/transform/transHundredMarkSystem'; +import { transDataForOverview } from '@common/transform/transDataForOverview'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import ImageFallback from '@common/components/ImageFallback'; +import { TransOpt } from '@modules/developer/type'; +import Tab from '@common/components/Tab'; + +const CalendarChart: React.FC = ({ + loading = false, + xAxis, + yAxis, +}) => { + 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: '代码', value: '2' }, + { label: 'PR', value: '3' }, + { label: 'Issues', value: '4' }, + ]; + const [onePointSys, setOnePointSys] = useState( + chartUserSettingState.onePointSys + ); + const [yAxisScale, setYAxisScale] = useState( + chartUserSettingState.yAxisScale + ); + + const echartsOpts = useMemo(() => { + const series = yAxis.map(({ legendName, data }) => { + !onePointSys && (data = data.map((i) => transHundredMarkSystem(i))); + return line({ name: legendName, data }); + }); + return getLineOption({ + xAxisData: xAxis, + series, + yAxis: { + type: 'value', + scale: yAxisScale, + }, + }); + }, [xAxis, yAxis, yAxisScale, onePointSys]); + + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + yAxisScale={yAxisScale} + onYAxisScaleChange={(v) => { + setYAxisScale(v); + }} + onePointSys={onePointSys} + /> + + )} + > + {(containerRef) => ( + <> + + + + // + )} + + ); +}; + +const dateKey = 'grimoireCreationDate'; + +const CalendarChartWithData = () => { + const { t } = useTranslation(); + const opts = [ + { + type: 'metricCodequality', + key: 'codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + }, + { + type: 'metricCommunity', + key: 'communitySupportScore', + legendName: t('metrics_models:community_service_and_support.title'), + }, + { + type: 'metricActivity', + key: 'activityScore', + legendName: t('metrics_models:activity.title'), + }, + ]; + + const optsWithOrg = [ + ...opts, + { + type: 'metricGroupActivity', + key: 'organizationsActivity', + legendName: t('metrics_models:organization_activity.title'), + }, + ]; + + const data = useMetricQueryData(); + + const isLoading = data.loading; + const copyOpts = optsWithOrg; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { xAxis, yAxisResult } = useMemo(() => { + const result = data.items[0].result; + if (!result) return { xAxis: [], yAxisResult: [] }; + + return transDataForOverview(result, copyOpts, dateKey, repoType); + }, [copyOpts, data, repoType]); + + return ( + + ); +}; + +export default CalendarChartWithData; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Cloud/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Cloud/index.tsx new file mode 100644 index 000000000..9581f52bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Cloud/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import CalendarChart from './CalendarChart'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + <> +
+
+ +
+
+ + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ; +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/OverviewSummary/DeveloperDashboard/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/DeveloperDashboard/index.tsx new file mode 100644 index 000000000..1892c4c43 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/DeveloperDashboard/index.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { useTranslation } from 'next-i18next'; +import { IoPeopleCircle, IoPersonCircle } from 'react-icons/io5'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import useQueryDateRange from '@modules/developer/hooks/useQueryDateRange'; +import { + useMetricDashboardQuery, + ContributorDetailOverview, +} from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { SiGitee, SiGithub } from 'react-icons/si'; +import { useRouter } from 'next/router'; +import { BiChat, BiGitPullRequest, BiGitCommit } from 'react-icons/bi'; +import { + AiFillClockCircle, + AiOutlineIssuesClose, + AiOutlineArrowRight, +} from 'react-icons/ai'; +import { GoIssueOpened, GoGitPullRequestClosed, GoRepo } from 'react-icons/go'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import BaseCard from '@common/components/BaseCard'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import Pie from '@modules/oh/components/Pie'; + +const DeveloperDashboard = () => { + 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]; + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + /> + + )} + > + {(containerRef) => ( + + // + )} + + ); + // return ( + // <> + //
+ // + // + //
+ // + // ); +}; + +const MetricBoxContributors = () => { + return ( +
+
+
+
+
+ +
+
Commits
+
+
{523}
+
+
+
+
+ +
+
PRs
+
+
{56}
+
+
+
+
+ +
+
Issues
+
+
{129}
+
+
+
+
+ +
+
Code Reviews
+
+
{71}
+
+
+
+
+ +
+
Contributed to
+
+
{236}
+
+
+
+ +
+
+ ); +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +const getIcons = (type: string) => { + switch (type) { + case 'github': + return ; + case 'gitee': + return ; + default: + return ; + } +}; + +export default DeveloperDashboard; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/Languages.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/Languages.tsx new file mode 100644 index 000000000..fba0327f4 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/Languages.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +const Languages = () => { + const languages = [ + { name: 'JavaScript', percentage: 40 }, + { name: 'Python', percentage: 30 }, + { name: 'Java', percentage: 16 }, + { name: 'C#', percentage: 8 }, + { name: 'C++', percentage: 4 }, + { name: 'Others', percentage: 2 }, + ]; + const colorList = [ + '#ef6667', + '#fcb32c', + '#409eff', + '#76d275', + '#5686a5', + '#505d96', + '#ededed', + ]; + return ( +
+
+ {languages.map((lang, index) => { + return ( +
+ ); + })} +
+
+ {languages.map((lang, index) => { + return ( +
+
+ {lang.name} {' ' + lang.percentage + '%'} +
+ ); + })} +
+
+ ); +}; + +export default Languages; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/index.tsx new file mode 100644 index 000000000..07c5bfa28 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Languages/index.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import Languages from './Languages'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import BaseCard from '@common/components/BaseCard'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + /> + + )} + > + {(containerRef) => ( + + // + )} + + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ; +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/OverviewSummary/LineChart.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/LineChart.tsx new file mode 100644 index 000000000..49c652966 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/LineChart.tsx @@ -0,0 +1,132 @@ +import React, { useMemo, useState } from 'react'; +import { ChartSummaryProps, getLineOption, line } from '@common/options'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import transHundredMarkSystem from '@common/transform/transHundredMarkSystem'; +import { transDataForOverview } from '@common/transform/transDataForOverview'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import ImageFallback from '@common/components/ImageFallback'; + +const LineChart: React.FC = ({ + loading = false, + xAxis, + yAxis, +}) => { + const { t } = useTranslation(); + const [onePointSys, setOnePointSys] = useState( + chartUserSettingState.onePointSys + ); + const [yAxisScale, setYAxisScale] = useState( + chartUserSettingState.yAxisScale + ); + + const echartsOpts = useMemo(() => { + const series = yAxis.map(({ legendName, data }) => { + !onePointSys && (data = data.map((i) => transHundredMarkSystem(i))); + return line({ name: legendName, data }); + }); + return getLineOption({ + xAxisData: xAxis, + series, + yAxis: { + type: 'value', + scale: yAxisScale, + }, + }); + }, [xAxis, yAxis, yAxisScale, onePointSys]); + + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + yAxisScale={yAxisScale} + onYAxisScaleChange={(v) => { + setYAxisScale(v); + }} + onePointSys={onePointSys} + /> + + )} + > + {(containerRef) => ( + + // + )} + + ); +}; + +const dateKey = 'grimoireCreationDate'; + +const LineChartWithData = () => { + const { t } = useTranslation(); + const opts = [ + { + type: 'metricCodequality', + key: 'codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + }, + { + type: 'metricCommunity', + key: 'communitySupportScore', + legendName: t('metrics_models:community_service_and_support.title'), + }, + { + type: 'metricActivity', + key: 'activityScore', + legendName: t('metrics_models:activity.title'), + }, + ]; + + const optsWithOrg = [ + ...opts, + { + type: 'metricGroupActivity', + key: 'organizationsActivity', + legendName: t('metrics_models:organization_activity.title'), + }, + ]; + + const data = useMetricQueryData(); + + const isLoading = data.loading; + const copyOpts = optsWithOrg; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { xAxis, yAxisResult } = useMemo(() => { + const result = data.items[0].result; + if (!result) return { xAxis: [], yAxisResult: [] }; + + return transDataForOverview(result, copyOpts, dateKey, repoType); + }, [copyOpts, data, repoType]); + + return ; +}; + +export default LineChartWithData; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/CalendarChart.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/CalendarChart.tsx new file mode 100644 index 000000000..667e02b08 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/CalendarChart.tsx @@ -0,0 +1,178 @@ +import React, { useMemo, useState } from 'react'; +import { ChartSummaryProps, getLineOption, line } from '@common/options'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import transHundredMarkSystem from '@common/transform/transHundredMarkSystem'; +import { transDataForOverview } from '@common/transform/transDataForOverview'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import ImageFallback from '@common/components/ImageFallback'; +import { TransOpt } from '@modules/developer/type'; +import Tab from '@common/components/Tab'; + +const CalendarChart: React.FC = ({ + loading = false, + xAxis, + yAxis, +}) => { + 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: '代码', value: '2' }, + { label: 'PR', value: '3' }, + { label: 'Issues', value: '4' }, + ]; + const [onePointSys, setOnePointSys] = useState( + chartUserSettingState.onePointSys + ); + const [yAxisScale, setYAxisScale] = useState( + chartUserSettingState.yAxisScale + ); + + const echartsOpts = useMemo(() => { + const series = yAxis.map(({ legendName, data }) => { + !onePointSys && (data = data.map((i) => transHundredMarkSystem(i))); + return line({ name: legendName, data }); + }); + return getLineOption({ + xAxisData: xAxis, + series, + yAxis: { + type: 'value', + scale: yAxisScale, + }, + }); + }, [xAxis, yAxis, yAxisScale, onePointSys]); + + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + yAxisScale={yAxisScale} + onYAxisScaleChange={(v) => { + setYAxisScale(v); + }} + onePointSys={onePointSys} + /> + + )} + > + {(containerRef) => ( + <> + + + + // + )} + + ); +}; + +const dateKey = 'grimoireCreationDate'; + +const CalendarChartWithData = () => { + const { t } = useTranslation(); + const opts = [ + { + type: 'metricCodequality', + key: 'codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + }, + { + type: 'metricCommunity', + key: 'communitySupportScore', + legendName: t('metrics_models:community_service_and_support.title'), + }, + { + type: 'metricActivity', + key: 'activityScore', + legendName: t('metrics_models:activity.title'), + }, + ]; + + const optsWithOrg = [ + ...opts, + { + type: 'metricGroupActivity', + key: 'organizationsActivity', + legendName: t('metrics_models:organization_activity.title'), + }, + ]; + + const data = useMetricQueryData(); + + const isLoading = data.loading; + const copyOpts = optsWithOrg; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { xAxis, yAxisResult } = useMemo(() => { + const result = data.items[0].result; + if (!result) return { xAxis: [], yAxisResult: [] }; + + return transDataForOverview(result, copyOpts, dateKey, repoType); + }, [copyOpts, data, repoType]); + + return ( + + ); +}; + +export default CalendarChartWithData; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/index.tsx new file mode 100644 index 000000000..9581f52bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/Radar/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import CalendarChart from './CalendarChart'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + <> +
+
+ +
+
+ + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ; +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/OverviewSummary/TopRepo/CalendarChart.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/CalendarChart.tsx new file mode 100644 index 000000000..4237cf500 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/CalendarChart.tsx @@ -0,0 +1,173 @@ +import React, { useMemo, useState } from 'react'; +import { ChartSummaryProps, getLineOption, line } from '@common/options'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import EChartX from '@common/components/EChartX'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import transHundredMarkSystem from '@common/transform/transHundredMarkSystem'; +import { transDataForOverview } from '@common/transform/transDataForOverview'; +import { Topic } from '@modules/developer/components/SideBar/config'; +import ScoreConversion from '@modules/developer/components/ScoreConversion'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import { chartUserSettingState } from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import ImageFallback from '@common/components/ImageFallback'; +import { TransOpt } from '@modules/developer/type'; +import Pie from './Pie'; + +const CalendarChart: React.FC = ({ + loading = false, + xAxis, + yAxis, +}) => { + 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: '代码', value: '2' }, + { label: 'PR', value: '3' }, + { label: 'Issues', value: '4' }, + ]; + const [onePointSys, setOnePointSys] = useState( + chartUserSettingState.onePointSys + ); + const [yAxisScale, setYAxisScale] = useState( + chartUserSettingState.yAxisScale + ); + + const echartsOpts = useMemo(() => { + const series = yAxis.map(({ legendName, data }) => { + !onePointSys && (data = data.map((i) => transHundredMarkSystem(i))); + return line({ name: legendName, data }); + }); + return getLineOption({ + xAxisData: xAxis, + series, + yAxis: { + type: 'value', + scale: yAxisScale, + }, + }); + }, [xAxis, yAxis, yAxisScale, onePointSys]); + + return ( + ( + <> + { + setFullScreen(b); + }} + enableReferenceLineSwitch={false} + yAxisScale={yAxisScale} + onYAxisScaleChange={(v) => { + setYAxisScale(v); + }} + onePointSys={onePointSys} + /> + + )} + > + {(containerRef) => ( +
+ +
+ + // + )} +
+ ); +}; + +const dateKey = 'grimoireCreationDate'; + +const CalendarChartWithData = () => { + const { t } = useTranslation(); + const opts = [ + { + type: 'metricCodequality', + key: 'codeQualityGuarantee', + legendName: t('metrics_models:collaboration_development_index.title'), + }, + { + type: 'metricCommunity', + key: 'communitySupportScore', + legendName: t('metrics_models:community_service_and_support.title'), + }, + { + type: 'metricActivity', + key: 'activityScore', + legendName: t('metrics_models:activity.title'), + }, + ]; + + const optsWithOrg = [ + ...opts, + { + type: 'metricGroupActivity', + key: 'organizationsActivity', + legendName: t('metrics_models:organization_activity.title'), + }, + ]; + + const data = useMetricQueryData(); + + const isLoading = data.loading; + const copyOpts = optsWithOrg; + const snap = useSnapshot(chartUserSettingState); + const repoType = snap.repoType; + const { xAxis, yAxisResult } = useMemo(() => { + const result = data.items[0].result; + if (!result) return { xAxis: [], yAxisResult: [] }; + + return transDataForOverview(result, copyOpts, dateKey, repoType); + }, [copyOpts, data, repoType]); + + return ( + + ); +}; + +export default CalendarChartWithData; diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/Pie.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/Pie.tsx new file mode 100644 index 000000000..ee7346060 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/Pie.tsx @@ -0,0 +1,208 @@ +import React, { useRef, useEffect } from 'react'; +import { graphic, init } from 'echarts'; + +const Pie = () => { + let data = [ + { name: 'flutter', value: 97 }, + { name: 'react', value: 68 }, + { name: 'vue', value: 50 }, + { name: 'axios', value: 36 }, + ]; + let xAxisData = data.map((item) => item.name); + let seriesData = data.map((item) => item.value); + let maxSeriesData = []; + const MAX = Math.max(...seriesData); + for (let i = 0; i < seriesData.length; i++) { + maxSeriesData.push(MAX); + } + maxSeriesData; + let barLinearColors = [ + '#ef6667', + '#fcb32c', + '#409eff', + '#76d275', + new graphic.LinearGradient(0, 1, 1, 1, [ + { offset: 0, color: '#EB3B5A' }, + { offset: 1, color: '#FE9C5A' }, + ]), + new graphic.LinearGradient(0, 1, 1, 1, [ + { offset: 0, color: '#FA8231' }, + { offset: 1, color: '#FFD14C' }, + ]), + new graphic.LinearGradient(0, 1, 1, 1, [ + { offset: 0, color: '#F7B731' }, + { offset: 1, color: '#FFEE96' }, + ]), + new graphic.LinearGradient(0, 1, 1, 1, [ + { offset: 0, color: '#0fe5e3' }, + { offset: 1, color: '#2ca1d6' }, + ]), + ]; + + function rankBarColor(cData) { + let tempData = []; + cData.forEach((item, index) => { + tempData.push({ + value: item, + itemStyle: { + color: index > 4 ? barLinearColors[3] : barLinearColors[index], + }, + }); + }); + return tempData; + } + var option = { + tooltip: { + backgroundColor: 'rgba(50,50,50,.3)', + textStyle: { + color: '#222', + }, + }, + grid: { + top: 20, + bottom: 20, + }, + xAxis: { + type: 'value', + splitLine: { show: false }, + axisLabel: { show: false }, + axisTick: { show: false }, + axisLine: { show: false }, + }, + yAxis: [ + { + type: 'category', + inverse: true, + axisLine: { show: false }, + axisTick: { show: false }, + data: xAxisData, + axisLabel: { + padding: [0, 8, 20, 0], + fontSize: 12, + rich: { + nt1: { + fontSize: 0, + color: '#fff', + backgroundColor: { + image: + 'https://avatars.githubusercontent.com/u/14101776?s=200&v=4', + }, + width: 25, + height: 25, + align: 'center', + borderRadius: 100, + }, + nt2: { + fontSize: 0, + color: '#fff', + backgroundColor: { + image: + 'https://avatars.githubusercontent.com/u/5550850?s=48&v=4', + }, + width: 25, + height: 25, + align: 'center', + borderRadius: 100, + }, + nt3: { + fontSize: 0, + color: '#fff', + backgroundColor: { + image: + 'https://avatars.githubusercontent.com/u/6128107?s=200&v=4', + }, + width: 25, + height: 25, + align: 'center', + borderRadius: 100, + }, + nt: { + fontSize: 0, + color: '#fff', + backgroundColor: { + image: 'https://avatars.githubusercontent.com/u/53640896?v=4', + }, + width: 25, + height: 25, + align: 'center', + borderRadius: 100, + }, + }, + formatter: function (value, index) { + let idx = index + 1; + if (idx <= 3) { + return ['{nt' + idx + '|' + idx + '}'].join('\n'); + } else { + return ['{nt|' + idx + '}'].join('\n'); + } + }, + }, + }, + { + //名称 + type: 'category', + offset: -10, + position: 'left', + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { + color: '#333', + align: 'left', + verticalAlign: 'bottom', + lineHeight: 32, + fontSize: 14, + padding: [-3, 4], + }, + data: xAxisData.reverse(), + }, + ], + series: [ + { + zlevel: 1, + type: 'bar', + barWidth: 8, + data: rankBarColor(seriesData), + itemStyle: { + normal: { + barBorderRadius: 30, + }, + }, + }, + { + type: 'bar', + barWidth: 8, + barGap: '-100%', + itemStyle: { + normal: { + barBorderRadius: 30, + color: 'rgba(0,0,0,0.04)', + }, + }, + label: { + normal: { + color: '#333', + show: true, + position: ['98%', '-15px'], + textStyle: { + fontSize: 14, + }, + formatter: function (params) { + return rankBarColor(seriesData)[params.dataIndex]['value']; + }, + }, + }, + data: maxSeriesData, + }, + ], + }; + + const cardRef = useRef(null); + + useEffect(() => { + let chart = init(cardRef.current); + chart.setOption(option); + }, [option]); + + return
; +}; +export default React.memo(Pie); diff --git a/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/index.tsx new file mode 100644 index 000000000..9581f52bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/TopRepo/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import CalendarChart from './CalendarChart'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + <> +
+
+ +
+
+ + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ; +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/OverviewSummary/index.tsx b/apps/web/src/modules/developer/DataView/OverviewSummary/index.tsx new file mode 100644 index 000000000..b06484c94 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/OverviewSummary/index.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { MetricQuery, SummaryQuery } from '@oss-compass/graphql'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import { Level } from '@modules/developer/constant'; +import DeveloperDashboard from './DeveloperDashboard'; +import Calendar from './Calendar'; +import Languages from './Languages'; +import LineChart from './LineChart'; +import TopRepo from './TopRepo'; +import Radar from './Radar'; +import Cloud from './Cloud'; + +import ConnectLineMini from '@modules/developer/components/ConnectLineMini'; + +const Overview: React.FC<{ + data: DeepReadonly< + { label: string; level: Level; result: MetricQuery | undefined }[] + >; +}> = ({ data }) => { + if (data.length == 1) { + return ( + <> +
+
+ +
+
+ + ); + } + + return null; +}; + +const OverviewSummary = () => { + const { items, loading } = useMetricQueryData(); + if (loading) { + return ; + } + return ( +
+ + + + {/* */} + + + +
+ ); +}; +const Loading = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); +export default withErrorBoundary(OverviewSummary, { + 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/RepoDataView/CodeMergeRatio.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CodeMergeRatio.tsx new file mode 100644 index 000000000..ab894d85e --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/CodeReviewRatio.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CodeReviewRatio.tsx new file mode 100644 index 000000000..f8302a8f6 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/CommitFrequency.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CommitFrequency.tsx new file mode 100644 index 000000000..a554ef9d1 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/CommitPRLinkedRatio.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CommitPRLinkedRatio.tsx new file mode 100644 index 000000000..70ef78a03 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/CommunityDropDownMenu.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CommunityDropDownMenu.tsx new file mode 100644 index 000000000..bffc38964 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/CommunityDropDownMenu.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; +import { ClickAwayListener } from '@mui/base/ClickAwayListener'; +import Popper from '@mui/material/Popper'; +import { AiFillCaretDown } from 'react-icons/ai'; +import classnames from 'classnames'; + +const CommunityDropDownMenu: React.FC<{ + type: string; + onTypeChange: (v: string) => void; +}> = ({ type, onTypeChange }) => { + const { t } = useTranslation(); + interface typeMap { + [key: string]: string; + } + const typeMap: typeMap = { + all: t('analyze:repos_type:all'), + governance: t('analyze:repos_type:governance_repository'), + 'software-artifact': t('analyze:repos_type:software_artifact_repository'), + }; + // + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setOpen((previousOpen) => !previousOpen); + }; + return ( + <> + { + if (!open) return; + setOpen(() => false); + }} + > +
+
handleClick(e)} + > + {typeMap[type]} + +
+ +
+ {Object.keys(typeMap).map((item, i) => { + return ( +
{ + onTypeChange(item); + setOpen(() => false); + }} + > + + {typeMap[item]} + +
+ ); + })} +
+
+
+
+ + ); +}; +export default CommunityDropDownMenu; diff --git a/apps/web/src/modules/developer/DataView/RepoDataView/CommunityRepos.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/CommunityRepos.tsx new file mode 100644 index 000000000..5d6cc7d27 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/CommunityRepos.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; +import BaseCard from '@common/components/BaseCard'; +import CommunityDropDownMenu from './CommunityDropDownMenu'; +import { useCommunityReposQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import MiniChart from '@common/components/EChartX/MiniChart'; +import { + getShortAnalyzeLink, + getFirstPathSegment, + toFixed, +} from '@common/utils'; +import Pagination from '@common/components/Antd/Pagination'; + +const RepoItem: React.FC<{ + name: string; + path: string; + backend: string; + shortCode: string; + type: string; + metricActivity: any[]; +}> = ({ name, path, shortCode, backend, type, metricActivity }) => { + const { t } = useTranslation(); + const data = Array.isArray(metricActivity) + ? metricActivity.map((i) => toFixed(i['activityScore'], 3)) + : []; + + return ( +
+ +

{name}

+ +

{getFirstPathSegment(path)}

+
+ {type === 'governance' ? ( +
+ {t('analyze:repos_type:governance_repository')} +
+ ) : ( +
+ {t('analyze:repos_type:software_artifact_repository')} +
+ )} + +
+
+ ); +}; + +const PRE_PAGE = 9; + +const CommunityRepos = () => { + const [page, setPage] = useState(1); + const { compareItems } = useCompareItems(); + const { t } = useTranslation(); + const [firstItem] = compareItems; + const [type, setType] = useState('all'); + + const { data, isLoading } = useCommunityReposQuery( + client, + { + label: firstItem?.label, + page: page, + per: PRE_PAGE, + type: type === 'all' ? '' : type, + }, + { enabled: Boolean(firstItem?.label) } + ); + + const trends = data?.communityOverview?.trends || []; + const count = data?.communityOverview?.projectsCount || 0; + const totalPage = Math.ceil(count / PRE_PAGE); + + return ( +
+ ( + { + setType(b); + setPage(1); + }} + /> + )} + > +
+ {trends.map((repo) => { + return ( + + ); + })} +
+ {totalPage > 1 && ( +
+ { + setPage(p); + }} + /> +
+ )} +
+
+ ); +}; + +export default CommunityRepos; diff --git a/apps/web/src/modules/developer/DataView/RepoDataView/ContributorCount.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/ContributorCount.tsx new file mode 100644 index 000000000..b40306793 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/IsMaintained.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/IsMaintained.tsx new file mode 100644 index 000000000..8a7a31726 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/LocFrequency.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/LocFrequency.tsx new file mode 100644 index 000000000..a8262bdc3 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/PRIssueLinked.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/PRIssueLinked.tsx new file mode 100644 index 000000000..c7d9cb1bc --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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/RepoDataView/TotalScore.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/TotalScore.tsx new file mode 100644 index 000000000..c175d8cae --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/TotalScore.tsx @@ -0,0 +1,87 @@ +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 ChartWithData from '@modules/developer/components/ChartWithData'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import ImageFallback from '@common/components/ImageFallback'; + +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 ( + ( + <> + { + 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/RepoDataView/TotalScoreRepo.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/TotalScoreRepo.tsx new file mode 100644 index 000000000..49b2cf5e5 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/TotalScoreRepo.tsx @@ -0,0 +1,87 @@ +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 ChartWithData from '@modules/developer/components/ChartWithData'; +import useGetLineOption from '@modules/developer/hooks/useGetLineOption'; +import CardDropDownMenu from '@modules/developer/components/CardDropDownMenu'; +import ImageFallback from '@common/components/ImageFallback'; + +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 ( + ( + <> + { + 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/RepoDataView/index.tsx b/apps/web/src/modules/developer/DataView/RepoDataView/index.tsx new file mode 100644 index 000000000..94856f9f9 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/RepoDataView/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 TotalScoreRepo from './TotalScoreRepo'; + +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 CommunityRepos from './CommunityRepos'; +import { withErrorBoundary } from 'react-error-boundary'; +import ErrorFallback from '@common/components/ErrorFallback'; +import ConnectLineMini from '@modules/developer/components/ConnectLineMini'; + +const CollaborationDevelopmentIndexOverview = () => { + const { t } = useTranslation(); + return ( + <> +
+ + + + + +
+ {/* +
+ + + + + + + + + +
*/} + + ); +}; + +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/Status/ErrorAnalysis.tsx b/apps/web/src/modules/developer/DataView/Status/ErrorAnalysis.tsx new file mode 100644 index 000000000..f77f846df --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Status/ErrorAnalysis.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; + +const ErrorAnalysis = () => { + const { t } = useTranslation(); + return ( +
+

+ {t('common:error.invalid_url_query')} +

+ + + {t('analyze:explore_other_projects')} + +
+ ); +}; + +export default ErrorAnalysis; diff --git a/apps/web/src/modules/developer/DataView/Status/LoadingAnalysis.tsx b/apps/web/src/modules/developer/DataView/Status/LoadingAnalysis.tsx new file mode 100644 index 000000000..baebdcc10 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Status/LoadingAnalysis.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const LoadingAnalysis = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); + +export default LoadingAnalysis; diff --git a/apps/web/src/modules/developer/DataView/Status/NotFoundAnalysis.tsx b/apps/web/src/modules/developer/DataView/Status/NotFoundAnalysis.tsx new file mode 100644 index 000000000..78b76e7a1 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Status/NotFoundAnalysis.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; + +const NotFoundAnalysis = () => { + const { t } = useTranslation(); + return ( +
+

+ {t('common:error.url_404')} +

+ + + {t('analyze:explore_other_projects')} + +
+ ); +}; + +export default NotFoundAnalysis; diff --git a/apps/web/src/modules/developer/DataView/Status/UnderAnalysis.tsx b/apps/web/src/modules/developer/DataView/Status/UnderAnalysis.tsx new file mode 100644 index 000000000..fab1076b9 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Status/UnderAnalysis.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; + +const UnderAnalysis = () => { + const { t } = useTranslation(); + return ( +
+
+ {'padding'} +
+

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

+ + {t('analyze:explore_other_projects')} + +
+ ); +}; + +export default UnderAnalysis; diff --git a/apps/web/src/modules/developer/DataView/Status/index.tsx b/apps/web/src/modules/developer/DataView/Status/index.tsx new file mode 100644 index 000000000..a7592bd73 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/Status/index.tsx @@ -0,0 +1,4 @@ +export { default as ErrorAnalysis } from './ErrorAnalysis'; +export { default as LoadingAnalysis } from './LoadingAnalysis'; +export { default as NotFoundAnalysis } from './NotFoundAnalysis'; +export { default as UnderAnalysis } from './UnderAnalysis'; diff --git a/apps/web/src/modules/developer/DataView/index.tsx b/apps/web/src/modules/developer/DataView/index.tsx new file mode 100644 index 000000000..ef71ff119 --- /dev/null +++ b/apps/web/src/modules/developer/DataView/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useStatusContext } from '@modules/developer/context'; +import { checkIsPending } from '@modules/developer/constant'; +import UnderAnalysis from './Status/UnderAnalysis'; +import NotFoundAnalysis from './Status/NotFoundAnalysis'; +import LoadingAnalysis from './Status/LoadingAnalysis'; +import Charts from './Charts'; + +const DataView = () => { + const { notFound, isLoading, status } = useStatusContext(); + + if (isLoading) { + return ; + } + + if (!notFound && checkIsPending(status)) { + return ; + } + + if (notFound) { + return ; + } + + return ( +
+ +
+ ); +}; + +export default DataView; diff --git a/apps/web/src/modules/developer/components/Badge.tsx b/apps/web/src/modules/developer/components/Badge.tsx new file mode 100644 index 000000000..97239376a --- /dev/null +++ b/apps/web/src/modules/developer/components/Badge.tsx @@ -0,0 +1,276 @@ +import React, { useState } from 'react'; +import cn from 'classnames'; +import { useRouter } from 'next/router'; +import { useCountDown } from 'ahooks'; +import { useTranslation } from 'next-i18next'; +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'; + +const queryMap = { + COLLAB_DEV_INDEX: 'collab_dev_index', + COMMUNITY: 'community', + ACTIVITY: 'activity', + ORGANIZATIONS_ACTIVITY: 'organizations_activity', +}; + +const anchorList = [ + { + badgeUrlQuery: queryMap.COLLAB_DEV_INDEX, + anchor: 'collaboration_development_index', + }, + { + badgeUrlQuery: queryMap.COMMUNITY, + anchor: 'community_service_support', + }, + { + badgeUrlQuery: queryMap.ACTIVITY, + anchor: 'community_activity', + }, + { + badgeUrlQuery: queryMap.ORGANIZATIONS_ACTIVITY, + anchor: 'organizations_activity', + }, +]; + +const Badge = () => { + const { t } = useTranslation(); + const router = useRouter(); + const slug = router.query.slugs as string; + const badgeLinks = { + logo: `/badge/${slug}.svg`, + collab_dev_index: `/badge/${slug}.svg?metric=${queryMap.COLLAB_DEV_INDEX}`, + community: `/badge/${slug}.svg?metric=${queryMap.COMMUNITY}`, + activity: `/badge/${slug}.svg?metric=${queryMap.ACTIVITY}`, + organizations_activity: `/badge/${slug}.svg?metric=${queryMap.ORGANIZATIONS_ACTIVITY}`, + }; + + const [open, setOpen] = useState(false); + + const [badgeSrc, setBadgeSrc] = useState(badgeLinks.logo); + + return ( + <> +
+ {t('analyze:function_menu')} +
+
{ + setOpen(true); + }} + > + {t('analyze:badge.title')} +
+ + { + setOpen(false); + }} + > +
+

{t('analyze:badge.title')}

+
{ + setOpen(false); + }} + > + +
+ { + setBadgeSrc(v); + }} + > +
+ +
+ +
+ {t('analyze:topic.productivity')} +
+
+ + +
+ +
+ {t('analyze:topic.robustness')} +
+
+ +
+ +
+ {t('analyze:topic.niche_creation')} +
+
+ +
+
+ +
+
+ + ); +}; + +const BadgeItem = ({ activeSrc, src }: { activeSrc: string; src: string }) => { + const isChecked = activeSrc === src; + return ( +
+ + + + +
+ ); +}; + +const getMarkdownAnchorLink = (badgeSrc: string) => { + const url = window.origin + window.location.pathname; + const item = anchorList.find((i) => badgeSrc.endsWith(i.badgeUrlQuery)); + if (!item) { + return url; + } + return `${url}#${item.anchor}`; +}; + +const TabPanel = ({ badgeSrc }: { badgeSrc: string }) => { + const { t } = useTranslation(); + const [tab, setTab] = React.useState('Markdown'); + const [targetDate, setTargetDate] = useState(); + const [countdown] = useCountDown({ targetDate }); + const badgeLink = window.origin + badgeSrc; + + let source = ''; + switch (tab) { + case 'Markdown': { + source = `[![OSS Compass Analyze](${badgeLink})](${getMarkdownAnchorLink( + badgeSrc + )})`; + 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 Badge; diff --git a/apps/web/src/modules/developer/components/CardDropDownMenu.tsx b/apps/web/src/modules/developer/components/CardDropDownMenu.tsx new file mode 100644 index 000000000..cbb48d1b6 --- /dev/null +++ b/apps/web/src/modules/developer/components/CardDropDownMenu.tsx @@ -0,0 +1,206 @@ +import React, { RefObject } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BsThreeDots } from 'react-icons/bs'; +import { AiOutlineLoading } from 'react-icons/ai'; +import Popper from '@mui/material/Popper'; +import { ClickAwayListener } from '@mui/base/ClickAwayListener'; +import Average from 'public/images/analyze/average.svg'; +import Median from 'public/images/analyze/median.svg'; +import YScale from 'public/images/analyze/y-scale.svg'; +import classnames from 'classnames'; +import { BiFullscreen, BiExitFullscreen } from 'react-icons/bi'; +import DownCardLoadImage from './DownCardLoadImage'; +import DownloadAndShare from './DownloadAndShare'; +import { subscribeKey } from 'valtio/utils'; +import { chartUserSettingState } from '@modules/developer/store'; + +interface CardDropDownMenuProps { + downloadImageSize?: 'middle' | 'full'; + cardRef: RefObject; + fullScreen?: boolean; + onFullScreen?: (v: boolean) => void; + + enableReferenceLineSwitch?: boolean; + showAvg?: boolean; + onAvgChange?: (v: boolean) => void; + showMedian?: boolean; + onMedianChange?: (v: boolean) => void; + + enableLineSettingSwitch?: boolean; + yAxisScale?: boolean; + onYAxisScaleChange?: (v: boolean) => void; + + onePointSys?: boolean; + yKey?: string; +} + +const CardDropDownMenu = (props: CardDropDownMenuProps) => { + const { + downloadImageSize = 'middle', + cardRef, + enableReferenceLineSwitch = true, + showAvg = false, + showMedian = false, + onMedianChange, + onAvgChange, + enableLineSettingSwitch = true, + yAxisScale, + onYAxisScaleChange, + onePointSys = false, + yKey = '', + } = 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); + }; + + subscribeKey(chartUserSettingState, 'showAvg', (v) => { + if (showAvg !== v) { + onAvgChange?.(v); + } + }); + subscribeKey(chartUserSettingState, 'showMedian', (v) => { + if (showMedian !== v) { + onMedianChange?.(v); + } + }); + subscribeKey(chartUserSettingState, 'yAxisScale', (v) => { + if (yAxisScale !== v) { + onYAxisScaleChange?.(v); + } + }); + + const canBeOpen = open && Boolean(anchorEl); + const id = canBeOpen ? 'transition-popper' : undefined; + + const ReferenceNode = enableReferenceLineSwitch ? ( + <> +
{ + onAvgChange?.(!showAvg); + }} + > + + {t('analyze:average')} +
+
{ + onMedianChange?.(!showMedian); + }} + > + + {t('analyze:median')} +
+ + ) : null; + + const LineSetting = enableLineSettingSwitch ? ( +
{ + onYAxisScaleChange?.(!yAxisScale); + }} + > + + {t('analyze:y_axis_scale')} +
+ ) : null; + + const FullScreen = ( +
{ + props.onFullScreen(!props.fullScreen); + setOpen((previousOpen) => !previousOpen); + }} + > + {props.fullScreen ? ( + <> + + + {t('analyze:full_screen_exit')} + + + ) : ( + <> + + + {t('analyze:full_screen')} + + + )} +
+ ); + + return ( + <> + { + if (!open) return; + setOpen(() => false); + }} + > +
+
handleClick(e)} + > + +
+ +
+ {ReferenceNode} + {LineSetting} + + {FullScreen} +
+
+ + {/* {loadingDownLoadImg && ( + { + setLoadingDownLoadImg(false); + }} + /> + )} */} +
+
+ + ); +}; + +export default CardDropDownMenu; diff --git a/apps/web/src/modules/developer/components/ChartWithData.tsx b/apps/web/src/modules/developer/components/ChartWithData.tsx new file mode 100644 index 000000000..743de15ca --- /dev/null +++ b/apps/web/src/modules/developer/components/ChartWithData.tsx @@ -0,0 +1,128 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'next-i18next'; +import transMetricToAxis from '@common/transform/transMetricToAxis'; +import transSummaryToAxis from '@common/transform/transSummaryToAxis'; +import { EChartsOption } from 'echarts'; +import useMetricQueryData from '@modules/developer/hooks/useMetricQueryData'; +import { + ChartThemeState, + chartThemeState, + chartUserSettingState, +} from '@modules/developer/store'; +import { useSnapshot } from 'valtio'; +import LinkLegacy from '@common/components/LinkLegacy'; +import { formatISO } from '@common/utils'; +import { TransOpt, GenChartData, YResult } from '@modules/developer/type'; +import { isNull, isUndefined } from 'lodash'; +import { Trans } from 'react-i18next'; + +const isEmptyData = (result: YResult[]) => { + return result.every((r) => { + return r.data.every((i) => { + return isNull(i) || isUndefined(i); + }); + }); +}; + +const ChartWithData: React.FC<{ + tansOpts: TransOpt; + getOptions: ( + input: GenChartData, + theme?: DeepReadonly + ) => EChartsOption; + indicators?: string; + children: + | ((args: { + loading: boolean; + isEmpty: boolean; + option: EChartsOption; + }) => ReactNode) + | ReactNode; +}> = ({ children, indicators, getOptions, tansOpts }) => { + const { t, i18n } = useTranslation(); + const theme = useSnapshot(chartThemeState); + const snap = useSnapshot(chartUserSettingState); + const data = useMetricQueryData(); + const loading = data?.loading; + + const { xAxis, yResults } = transMetricToAxis( + data?.items, + tansOpts, + snap.repoType + ); + + const { summaryMean, summaryMedian } = transSummaryToAxis( + data?.summary, + xAxis, + tansOpts.summaryKey + ); + + const compareLabels = yResults.map((i) => i.label); + const isCompare = yResults.length > 1; + + // todo split chart options in different components + const echartsOpts = getOptions( + { + 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, + }; + } + + const isEmpty = isEmptyData(yResults); + let EmptyNode = null; + if (isEmpty && !loading) { + EmptyNode = ( +
+

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

+

+ + ), + }} + values={{ + e: t('analyze:contact_us'), + }} + /> +

+
+ ); + } + + return ( + <> + {typeof children === 'function' + ? children({ loading, isEmpty, option: echartsOpts }) + : children} + + {EmptyNode} + + ); +}; + +export default ChartWithData; diff --git a/apps/web/src/modules/developer/components/CompareBar/AddInput.tsx b/apps/web/src/modules/developer/components/CompareBar/AddInput.tsx new file mode 100644 index 000000000..0311babda --- /dev/null +++ b/apps/web/src/modules/developer/components/CompareBar/AddInput.tsx @@ -0,0 +1,173 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import SearchDropdown from './SearchDropdown'; +import { useThrottle } from 'ahooks'; +import { useClickAway } from 'react-use'; +import gsap from 'gsap'; +import { SearchQuery, useSearchQuery } from '@oss-compass/graphql'; +import client from '@common/gqlClient'; +import { AiOutlineLoading, AiOutlinePlus } from 'react-icons/ai'; +import { Level } from '@modules/developer/constant'; +import useCompareItems from '@modules/developer/hooks/useCompareItems'; +import { removeHttps } from '@common/utils'; +import { compareIdsAdd } from '@common/utils/links'; +import { getRouteAsPath } from '@common/utils/url'; +import qs from 'query-string'; + +const checkLevel = (shortId: string) => { + if (shortId.startsWith('s')) { + return Level.REPO; + } + if (shortId.startsWith('c')) { + return Level.REPO; + } + return Level.REPO; +}; + +const AddInput = () => { + const { t } = useTranslation(); + const search = window.location.search; + const ref = useRef(null); + const inputRef = useRef(null); + + const router = useRouter(); + const { compareItems, compareSlugs } = useCompareItems(); + const firstItem = compareItems[0]; + const level = firstItem.level; + + const q = gsap.utils.selector(ref); + + const [confirmItem, setConfirmItem] = useState< + SearchQuery['fuzzySearch'][number] | null + >(null); + + const [keyword, setKeyword] = useState(''); + + const throttledKeyword = useThrottle(keyword, { wait: 300 }); + const { isLoading, data, fetchStatus } = useSearchQuery( + client, + { + keyword: throttledKeyword, + level, + }, + { enabled: Boolean(throttledKeyword) } + ); + const showLoading = isLoading && fetchStatus === 'fetching'; + + useEffect(() => { + setKeyword(''); + }, [search]); + + const resetInput = () => { + setKeyword(''); + setConfirmItem(null); + + gsap.to(q('#search-input'), { display: 'none', left: 30, duration: 0 }); + gsap.to(q('#add-compare-btn'), { display: 'flex', duration: 0 }); + gsap.to(ref.current, { + width: 96, + duration: 0.2, + }); + }; + + useClickAway(ref, () => { + resetInput(); + }); + + const placeholder = React.useMemo(() => { + if (level === Level.REPO) { + return t('analyze:search_repo_input_placeholder'); + } + if (level === Level.COMMUNITY) { + return t('analyze:search_community_input_placeholder'); + } + return 'search'; + }, [level, t]); + + return ( +
+
+
+
+ { + 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/ConnectLineMini.tsx b/apps/web/src/modules/developer/components/ConnectLineMini.tsx new file mode 100644 index 000000000..b817549b3 --- /dev/null +++ b/apps/web/src/modules/developer/components/ConnectLineMini.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const ConnectLineMini = () => { + return ( +
+
+
+ ); +}; + +export default ConnectLineMini; 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} + /> + + ) : ( +