From 7efc083601dde5735133c4b2d6eeb0e212ba9cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allen=20Zhang=20=28=E5=BC=A0=E6=B6=9B=29?= Date: Mon, 5 Feb 2024 15:36:42 +0800 Subject: [PATCH] feat: projects using native sql query sorting --- packages/canyon-backend/schema.gql | 17 ++- .../src/prisma/prisma.service.ts | 4 +- .../src/project/project.resolver.ts | 16 +- .../project/services/get-projects.service.ts | 139 +++++++----------- .../src/types/input-types.args.ts | 29 ++++ .../backend/gql/queries/GetProjects.graphql | 4 +- .../src/pages/index/projects/index.tsx | 37 +++-- 7 files changed, 130 insertions(+), 116 deletions(-) create mode 100644 packages/canyon-backend/src/types/input-types.args.ts diff --git a/packages/canyon-backend/schema.gql b/packages/canyon-backend/schema.gql index 67301108..d7ef9272 100644 --- a/packages/canyon-backend/schema.gql +++ b/packages/canyon-backend/schema.gql @@ -168,7 +168,22 @@ type Query { me: User! """获取Project""" - getProjects(current: Int!, pageSize: Int!, keyword: String!, bu: [String!]!): ProjectPagesModel! + getProjects( + keyword: String! + bu: [String!]! + + """当前页码""" + current: Int! + + """每页数量""" + pageSize: Int! + + """排序字段名称""" + field: String! + + """升序或降序""" + order: String! + ): ProjectPagesModel! """获取Projects部门选项""" getProjectsBuOptions: [BuOption!]! diff --git a/packages/canyon-backend/src/prisma/prisma.service.ts b/packages/canyon-backend/src/prisma/prisma.service.ts index 8c10940d..43bd8f37 100755 --- a/packages/canyon-backend/src/prisma/prisma.service.ts +++ b/packages/canyon-backend/src/prisma/prisma.service.ts @@ -7,7 +7,9 @@ export class PrismaService implements OnModuleInit, OnModuleDestroy { constructor() { - super(); + super({ + // log: ['query', 'info', 'warn', 'error'], + }); } async onModuleInit() { await this.$connect(); diff --git a/packages/canyon-backend/src/project/project.resolver.ts b/packages/canyon-backend/src/project/project.resolver.ts index d0447027..a61e6e29 100755 --- a/packages/canyon-backend/src/project/project.resolver.ts +++ b/packages/canyon-backend/src/project/project.resolver.ts @@ -8,7 +8,6 @@ import { ProjectPagesModel } from './models/project-pages.model'; import { ProjectService } from './services/project.service'; import { GetProjectChartDataService } from './services/get-project-chart-data.service'; import { ProjectChartDataModel } from './models/project-chart-data.model'; -import { ProjectRecordsModel } from './models/project-records.model'; import { GetProjectRecordsService } from './services/get-project-records.service'; import { ProjectRecordsPagesModel } from './models/project-records-pages.model'; import { GetProjectCompartmentDataService } from './services/get-project-compartment-data.service'; @@ -17,6 +16,7 @@ import { GetProjectRecordDetailByShaService } from './services/get-project-recor import { ProjectRecordDetailModel } from './models/project-record-detail.model'; import { Project2 } from './project2.model'; import { GetProjectsService } from './services/get-projects.service'; +import { PaginationArgs, SorterArgs } from '../types/input-types.args'; @Resolver(() => 'Project') export class ProjectResolver { constructor( @@ -31,12 +31,20 @@ export class ProjectResolver { description: '获取Project', }) getProjects( - @Args('current', { type: () => Int }) current: number, - @Args('pageSize', { type: () => Int }) pageSize: number, @Args('keyword', { type: () => String }) keyword: string, @Args('bu', { type: () => [String] }) bu: string[], + @Args() paginationArgs: PaginationArgs, + @Args() sorterArgs: SorterArgs, ): Promise { - return this.getProjectsService.invoke(current, pageSize, keyword, bu); + // @ts-ignore + return this.getProjectsService.invoke( + paginationArgs.current, + paginationArgs.pageSize, + keyword, + bu, + sorterArgs.field, + sorterArgs.order, + ); } @Query(() => [BuOption], { diff --git a/packages/canyon-backend/src/project/services/get-projects.service.ts b/packages/canyon-backend/src/project/services/get-projects.service.ts index e1cbb1c5..8600ed2e 100644 --- a/packages/canyon-backend/src/project/services/get-projects.service.ts +++ b/packages/canyon-backend/src/project/services/get-projects.service.ts @@ -1,100 +1,61 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; -import { Project } from '../project.model'; -import { percent } from '../../utils/utils'; -import { getProjectByID } from 'src/adapter/gitlab.adapter'; -// import { getProjectByID } from '../adapter/gitlab.adapter'; -function parseGitLabUrl(gitLabUrl) { - // 匹配 GitLab URL 的正则表达式 - const gitLabRegex = /^(?:https?:\/\/)?([^\/]+)\/(.+)$/; - - // 尝试匹配正则表达式 - const match = gitLabUrl.match(gitLabRegex); - - if (match) { - // 提取匹配的组和仓库名 - const groupAndRepo = match[2].split('/'); - const groupName = groupAndRepo.slice(0, -1).join('/'); - const repositoryName = groupAndRepo.slice(-1)[0]; - return { groupName, repositoryName }; - } else { - // 如果没有匹配到,返回 null - return { groupName: null, repositoryName: null }; - } +import { Prisma } from '@prisma/client'; +function underscoreToCamelCase(string) { + return string.replace(/_([a-z])/g, function (match, letter) { + return letter.toUpperCase(); + }); +} +function camelCaseToUnderscore(string) { + return string.replace(/([A-Z])/g, '_$1').toLowerCase(); } - @Injectable() export class GetProjectsService { constructor(private readonly prisma: PrismaService) {} - async invoke(current, pageSize, keyword, bu) { - const whereCondition: any = { - OR: [ - // { description: { contains: keyword } }, - // { name: { contains: keyword } }, - { pathWithNamespace: { contains: keyword, mode: 'insensitive' } }, - { id: { contains: keyword, mode: 'insensitive' } }, - ], - }; - - if (bu.length > 0) { - whereCondition['bu'] = { in: bu }; - } - const total = await this.prisma.project.count({ - where: whereCondition, - }); - - const projects = await this.prisma.project.findMany({ - where: whereCondition, - skip: (current - 1) * pageSize, - take: pageSize - 0, - }); - - const rows = []; - - for (let i = 0; i < projects.length; i++) { - const coverages = await this.prisma.coverage.findMany({ - where: { - projectID: projects[i].id, - covType: 'all', - }, - }); + async invoke(current, pageSize, keyword, _bu, field, order) { + const bu = + _bu.length > 0 + ? _bu + : await this.prisma.project + .groupBy({ + by: ['bu'], + _count: { + bu: true, + }, + }) + .then((r) => r.map((item) => item.bu)); + + const limit = pageSize; // pageSize控制每页的记录数 + const offset = (current - 1) * pageSize; // 计算要跳过的记录数 + + const total: any = await this.prisma.$queryRaw` + SELECT COUNT(DISTINCT p.id) AS total +FROM project_20240130 AS p +LEFT JOIN coverage_20240130 AS c ON p.id = c.project_id +LEFT JOIN summary_20240130 AS s ON c.sha = s.sha and c.report_id=s.report_id +WHERE s.metric_type='statements' AND s.cov_type='all' AND bu IN (${Prisma.join(bu)}) AND (p.path_with_namespace ILIKE ${'%' + keyword + '%'} OR p.id ILIKE ${'%' + keyword + '%'}) +GROUP BY p.id + `.then((r: any) => r.length); + + const rows: any[] = await this.prisma.$queryRaw` +SELECT p.id AS id, p.path_with_namespace,p.description,p.name, CAST(COUNT(DISTINCT c.sha) AS INT) AS report_times,p.bu, ROUND(MAX(100*covered::decimal / NULLIF(total, 0)),2) AS max_coverage, MAX(s.created_at) AS last_report_time +FROM project_20240130 AS p +LEFT JOIN coverage_20240130 AS c ON p.id = c.project_id +LEFT JOIN summary_20240130 AS s ON c.sha = s.sha and c.report_id=s.report_id +WHERE s.metric_type='statements' AND s.cov_type='all' AND bu IN (${Prisma.join(bu)}) AND (p.path_with_namespace ILIKE ${'%' + keyword + '%'} OR p.id ILIKE ${'%' + keyword + '%'}) +GROUP BY p.id +ORDER BY ${Prisma.sql([camelCaseToUnderscore(field) || 'last_report_time'])} ${Prisma.sql([{ ascend: 'ASC', descend: 'DESC' }[order] || 'DESC'])} +LIMIT ${limit} OFFSET ${offset}; +`; - const summarys = await this.prisma.summary.findMany({ - where: { - // projectID: projects[i].id, - covType: 'all', - metricType: 'statements', - sha: { - in: [...new Set(coverages.map((item) => item.sha))], - }, - }, - orderBy: { - createdAt: 'desc', - }, - }); - if (summarys.length > 0) { - rows.push({ - ...projects[i], - lastReportTime: summarys[0]?.createdAt, - reportTimes: summarys.length, - maxCoverage: Math.max( - ...summarys.map((item) => - item.total === 0 ? 0 : percent(item.covered, item.total), - ), - ), - }); - } else { - rows.push({ - ...projects[i], - lastReportTime: new Date(), - reportTimes: 0, - maxCoverage: 0, - }); - } - } return { - data: rows, - total, + data: rows.map((item: any) => { + return Object.keys(item).reduce((acc, key) => { + acc[underscoreToCamelCase(key)] = item[key]; + return acc; + }, {}); + }), + total: total, }; } } diff --git a/packages/canyon-backend/src/types/input-types.args.ts b/packages/canyon-backend/src/types/input-types.args.ts new file mode 100644 index 00000000..c9a0f0c3 --- /dev/null +++ b/packages/canyon-backend/src/types/input-types.args.ts @@ -0,0 +1,29 @@ +import { ArgsType, Field, ID, InputType, Int } from '@nestjs/graphql'; + +@ArgsType() +@InputType() +export class PaginationArgs { + @Field(() => Int, { + description: '当前页码', + }) + current: number; + + @Field(() => Int, { + description: '每页数量', + }) + pageSize: number; +} + +@ArgsType() +@InputType() +export class SorterArgs { + @Field({ + description: '排序字段名称', + }) + field: string; + + @Field({ + description: '升序或降序', + }) + order: string; +} diff --git a/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql b/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql index 1f8677a8..f7eba1c3 100755 --- a/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql +++ b/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql @@ -1,5 +1,5 @@ -query GetProjects($current: Int!, $pageSize: Int!, $keyword: String!,$bu: [String!]!) { - getProjects(current:$current, pageSize:$pageSize, keyword:$keyword,bu:$bu) { +query GetProjects($current: Int!, $pageSize: Int!, $keyword: String!,$bu: [String!]!,$field: String!,$order: String!) { + getProjects(current:$current, pageSize:$pageSize, keyword:$keyword,bu:$bu,field:$field,order:$order) { total data { id diff --git a/packages/canyon-platform/src/pages/index/projects/index.tsx b/packages/canyon-platform/src/pages/index/projects/index.tsx index cc839eb2..8d6f84ce 100755 --- a/packages/canyon-platform/src/pages/index/projects/index.tsx +++ b/packages/canyon-platform/src/pages/index/projects/index.tsx @@ -4,7 +4,7 @@ import { Button, Card, Divider, Popconfirm, Select, Table, Typography } from 'an import Search from 'antd/es/input/Search'; import { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; -import { useEffect, useMemo, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -34,15 +34,18 @@ const ProjectPage = () => { { title: 'Bu', dataIndex: 'bu', + sorter: true, }, { title: t('Report Times'), dataIndex: 'reportTimes', + sorter: true, }, { title: 'Max coverage', dataIndex: 'maxCoverage', key: 'maxCoverage', + sorter: true, render: (text) => { return {text}%; }, @@ -50,6 +53,7 @@ const ProjectPage = () => { { title: t('Last Report Time'), dataIndex: 'lastReportTime', + sorter: true, render(_) { return {dayjs(_).format('YYYY-MM-DD HH:mm')}; }, @@ -102,12 +106,16 @@ const ProjectPage = () => { return []; } })(); - // const [loading, setLoading] = useState(false); const [keyword, setKeyword] = useState(''); const [bu, setBu] = useState(initBu); const [current, setCurrent] = useState(1); const [pageSize, setPageSize] = useState(10); - // const [loading, setLoading] = useState(false); + const [sorter, setSorter] = useState({}); + + const { data: projectsBuOptionsData } = useQuery(GetProjectsBuOptionsDocument, { + fetchPolicy: 'no-cache', + }); + const { data: projectsData, loading, @@ -118,24 +126,12 @@ const ProjectPage = () => { pageSize: pageSize, keyword: keyword, bu: bu, + field: sorter.field || '', + order: sorter.order || '', }, fetchPolicy: 'no-cache', }); - const { - data: projectsBuOptionsData, - // loading, - // refetch, - } = useQuery(GetProjectsBuOptionsDocument, { - fetchPolicy: 'no-cache', - }); - - useEffect(() => { - // refetch(); - }, []); - - // const s = useMemo(() => {}, [projectsData]); - return (
@@ -144,7 +140,6 @@ const ProjectPage = () => { {t('menus.projects')} - {/*{t('projects.desc')}*/}
@@ -191,7 +186,11 @@ const ProjectPage = () => { bordered={false} dataSource={projectsData?.getProjects?.data || []} columns={columns} - onChange={(val) => { + onChange={(val, _, _sorter:any) => { + setSorter({ + field: _sorter.field, + order: _sorter.order, + }); setCurrent(val.current || 1); setPageSize(val.pageSize || 10); }}