diff --git a/ent/gen/ent/gql_collection.go b/ent/gen/ent/gql_collection.go index a14f204..25380fe 100644 --- a/ent/gen/ent/gql_collection.go +++ b/ent/gen/ent/gql_collection.go @@ -3524,6 +3524,28 @@ func newTargetPairPaginateArgs(rv map[string]any) *targetpairPaginateArgs { if v := rv[beforeField]; v != nil { args.before = v.(*Cursor) } + if v, ok := rv[orderByField]; ok { + switch v := v.(type) { + case map[string]any: + var ( + err1, err2 error + order = &TargetPairOrder{Field: &TargetPairOrderField{}, Direction: entgql.OrderDirectionAsc} + ) + if d, ok := v[directionField]; ok { + err1 = order.Direction.UnmarshalGQL(d) + } + if f, ok := v[fieldField]; ok { + err2 = order.Field.UnmarshalGQL(f) + } + if err1 == nil && err2 == nil { + args.opts = append(args.opts, WithTargetPairOrder(order)) + } + case *TargetPairOrder: + if v != nil { + args.opts = append(args.opts, WithTargetPairOrder(v)) + } + } + } if v, ok := rv[whereField].(*TargetPairWhereInput); ok { args.opts = append(args.opts, WithTargetPairFilter(v.Filter)) } diff --git a/ent/gen/ent/gql_pagination.go b/ent/gen/ent/gql_pagination.go index 23a6892..8fcd783 100644 --- a/ent/gen/ent/gql_pagination.go +++ b/ent/gen/ent/gql_pagination.go @@ -8378,6 +8378,53 @@ func (tp *TargetPairQuery) Paginate( return conn, nil } +var ( + // TargetPairOrderFieldDurationInMs orders TargetPair by duration_in_ms. + TargetPairOrderFieldDurationInMs = &TargetPairOrderField{ + Value: func(tp *TargetPair) (ent.Value, error) { + return tp.DurationInMs, nil + }, + column: targetpair.FieldDurationInMs, + toTerm: targetpair.ByDurationInMs, + toCursor: func(tp *TargetPair) Cursor { + return Cursor{ + ID: tp.ID, + Value: tp.DurationInMs, + } + }, + } +) + +// String implement fmt.Stringer interface. +func (f TargetPairOrderField) String() string { + var str string + switch f.column { + case TargetPairOrderFieldDurationInMs.column: + str = "DURATION" + } + return str +} + +// MarshalGQL implements graphql.Marshaler interface. +func (f TargetPairOrderField) MarshalGQL(w io.Writer) { + io.WriteString(w, strconv.Quote(f.String())) +} + +// UnmarshalGQL implements graphql.Unmarshaler interface. +func (f *TargetPairOrderField) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("TargetPairOrderField %T must be a string", v) + } + switch str { + case "DURATION": + *f = *TargetPairOrderFieldDurationInMs + default: + return fmt.Errorf("%s is not a valid TargetPairOrderField", str) + } + return nil +} + // TargetPairOrderField defines the ordering field of TargetPair. type TargetPairOrderField struct { // Value extracts the ordering value from the given TargetPair. diff --git a/ent/schema/targetpair.go b/ent/schema/targetpair.go index 4f4e9c2..9d8baad 100644 --- a/ent/schema/targetpair.go +++ b/ent/schema/targetpair.go @@ -1,7 +1,9 @@ package schema import ( + "entgo.io/contrib/entgql" "entgo.io/ent" + "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) @@ -15,17 +17,23 @@ type TargetPair struct { func (TargetPair) Fields() []ent.Field { return []ent.Field{ // The label of the target ex: //foo:bar. - field.String("label").Optional(), + field.String("label"). + Optional(), // Duration in Milliseconds. // Time from target configured message received and processed until target completed message received and processed, calculated on build complete - field.Int64("duration_in_ms").Optional(), + field.Int64("duration_in_ms"). + Optional(). + Annotations(entgql.OrderField("DURATION")), // Overall success of the target (defaults to false). - field.Bool("success").Optional().Default(false), + field.Bool("success"). + Optional(). + Default(false), // The target kind if available. - field.String("target_kind").Optional(), + field.String("target_kind"). + Optional(), // The size of the test, if the target is a test target. Unset otherwise. field.Enum("test_size"). @@ -72,3 +80,11 @@ func (TargetPair) Edges() []ent.Edge { Unique(), } } + +// Annotations of the TargetPair +func (TargetPair) Annotations() []schema.Annotation { + return []schema.Annotation{ + entgql.RelayConnection(), + entgql.QueryField("findTargets"), + } +} diff --git a/frontend/src/app/bazel-invocations/[invocationID]/index.graphql.ts b/frontend/src/app/bazel-invocations/[invocationID]/index.graphql.ts index a5ddedf..3ed40f2 100644 --- a/frontend/src/app/bazel-invocations/[invocationID]/index.graphql.ts +++ b/frontend/src/app/bazel-invocations/[invocationID]/index.graphql.ts @@ -8,6 +8,24 @@ export const LOAD_FULL_BAZEL_INVOCATION_DETAILS = gql(/* GraphQL */ ` } `); +export const GET_PROBLEM_DETAILS = gql(/* GraphQL */ ` + query GetProblemDetails($invocationID: String!) { + bazelInvocation(invocationId: $invocationID) { + ...ProblemDetails + } + } +`); + +export const PROBLEM_DETAILS_FRAGMENT = gql(/* GraphQL */` + + fragment ProblemDetails on BazelInvocation{ + problems { + ...ProblemInfo + } + } + +`) + export const BAZEL_INVOCATION_FRAGMENT = gql(/* GraphQL */ ` fragment BazelInvocationInfo on BazelInvocation { metrics { @@ -212,6 +230,9 @@ fragment BazelInvocationInfo on BazelInvocation { } id } + configurationMnemonic + cpu + numFetches stepLabel sourceControl { id @@ -287,16 +308,11 @@ fragment BlobReferenceInfo on BlobReference { export const FULL_BAZEL_INVOCATION_DETAILS = gql(/* GraphQL */ ` fragment FullBazelInvocationDetails on BazelInvocation { - problems { - ...ProblemInfo - } ...BazelInvocationInfo } `); - - export const GET_ACTION_PROBLEM = gql(/* GraphQL */ ` query GetActionProblem($id: ID!) { node(id: $id) { diff --git a/frontend/src/app/bazel-invocations/[invocationID]/page.tsx b/frontend/src/app/bazel-invocations/[invocationID]/page.tsx index afa10e4..38a083b 100644 --- a/frontend/src/app/bazel-invocations/[invocationID]/page.tsx +++ b/frontend/src/app/bazel-invocations/[invocationID]/page.tsx @@ -40,6 +40,11 @@ const BazelInvocationsContent: React.FC = ({ loading, error, networkStatu ); } + if (loading && networkStatus !== NetworkStatus.poll) { + return ( + + ); + } if (error && invocationInfo) { return ( <> @@ -50,12 +55,7 @@ const BazelInvocationsContent: React.FC = ({ loading, error, networkStatu } if (invocationInfo) { - return - - - + return } return <> @@ -78,7 +78,7 @@ const Page: React.FC = ({ params }) => { const invocation = getFragmentData(FULL_BAZEL_INVOCATION_DETAILS, data?.bazelInvocation); const invocationOverview = getFragmentData(BAZEL_INVOCATION_FRAGMENT, invocation) - const problems = invocation?.problems.map(p => getFragmentData(PROBLEM_INFO_FRAGMENT, p)) + const stop = shouldStopPolling(invocation); useEffect(() => { @@ -91,7 +91,7 @@ const Page: React.FC = ({ params }) => { return ( } + content={} /> ); } diff --git a/frontend/src/app/targets/[slug]/graphql.ts b/frontend/src/app/targets/[slug]/graphql.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/targets/[slug]/page.tsx b/frontend/src/app/targets/[slug]/page.tsx new file mode 100644 index 0000000..3c9a64f --- /dev/null +++ b/frontend/src/app/targets/[slug]/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import React from 'react'; +import Content from '@/components/Content'; +import PortalCard from '@/components/PortalCard'; +import { Space } from 'antd'; +import { DeploymentUnitOutlined } from '@ant-design/icons'; +import TargetDetails from '@/components/Targets/TargetDetails'; + +interface PageParams { + params: { + slug: string + } +} + +const Page: React.FC = ({ params }) => { + const label = decodeURIComponent(atob(decodeURIComponent(params.slug))) + return ( + + } + titleBits={[Target Details]} + > + + + + } + /> + ); +} + +export default Page; diff --git a/frontend/src/app/targets/graphql.tsx b/frontend/src/app/targets/graphql.tsx new file mode 100644 index 0000000..8773bc2 --- /dev/null +++ b/frontend/src/app/targets/graphql.tsx @@ -0,0 +1,61 @@ +import { gql } from "@/graphql/__generated__"; + +export const GET_TARGETS_DATA = gql(/* GraphQl */` +query GetTargetsWithOffset( + $label: String, + $offset: Int, + $limit: Int, + $sortBy: String, + $direction: String) { + getTargetsWithOffset( + label: $label + offset: $offset + limit: $limit + sortBy: $sortBy + direction: $direction + ) { + total + result { + label + sum + min + max + avg + count + passRate + } + } + } +`); + +export const FIND_TARGETS = gql(/* GraphQL */ ` + query FindTargets( + $first: Int! + $where: TargetPairWhereInput + $orderBy: TargetPairOrder + $after: Cursor + ){ + findTargets (first: $first, where: $where, orderBy: $orderBy, after: $after){ + totalCount + pageInfo{ + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + node { + id + durationInMs + label + success + bazelInvocation { + invocationID + } + } + } + } + } + `); + +export default GET_TARGETS_DATA; diff --git a/frontend/src/app/targets/page.tsx b/frontend/src/app/targets/page.tsx new file mode 100644 index 0000000..721ba51 --- /dev/null +++ b/frontend/src/app/targets/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import React from 'react'; +import Content from '@/components/Content'; +import PortalCard from '@/components/PortalCard'; +import { Space } from 'antd'; +import { DeploymentUnitOutlined } from '@ant-design/icons'; +import TargetGrid from '@/components/Targets/TargetGrid'; + +const Page: React.FC = () => { + return ( + + } + titleBits={[Targets Overview]}> + + + + } + /> + ); +} + +export default Page; diff --git a/frontend/src/components/ActionDataMetrics/index.tsx b/frontend/src/components/ActionDataMetrics/index.tsx index 4d21b18..ec69d79 100644 --- a/frontend/src/components/ActionDataMetrics/index.tsx +++ b/frontend/src/components/ActionDataMetrics/index.tsx @@ -124,7 +124,7 @@ const ActionDataMetrics: React.FC<{ acMetrics: ActionSummary | undefined; }> = ( */} - } titleBits={["User Time Breakdown"]}> + } titleBits={["User Time Breakdown"]} hidden={totalUserTime == 0}> = ({ invocationOverview, problems, children, isNestedWithinBuildCard }) => { +}> = ({ invocationOverview, isNestedWithinBuildCard }) => { const { invocationID, build, @@ -74,6 +79,9 @@ const BazelInvocation: React.FC<{ metrics, testCollection, targets, + numFetches, + cpu, + configurationMnemonic //stepLabel, //relatedFiles, @@ -129,23 +137,66 @@ const BazelInvocation: React.FC<{ titleBits.push(); } + + const hideTestsTab: boolean = (testCollection?.length ?? 0) == 0 + const hideTargetsTab: boolean = (targetData?.length ?? 0) == 0 ? true : false + const hideNetworkTab: boolean = bytesRecv == 0 && bytesSent == 0 + const hideSourceControlTab: boolean = sourceControl?.runID == undefined || sourceControl.runID == null || sourceControl.runID == "" ? true : false + const hideLogsTab: boolean = true + const hideMemoryTab: boolean = (memoryMetrics?.peakPostGcHeapSize ?? 0) == 0 && (memoryMetrics?.peakPostGcHeapSize ?? 0) == 0 && (memoryMetrics?.usedHeapSizePostBuild ?? 0) == 0 + const hideProblemsTab: boolean = exitCode?.name == "SUCCESS" + const hideArtifactsTab: boolean = (artifactMetrics?.outputArtifactsSeen?.count ?? 0) == 0 && (artifactMetrics?.sourceArtifactsRead?.count ?? 0) == 0 && (artifactMetrics?.outputArtifactsFromActionCache?.count ?? 0) == 0 && (artifactMetrics?.topLevelArtifacts?.count ?? 0) == 0 + const hideActionsDataTab: boolean = acMetrics?.actionsExecuted == 0 + const hideRunnersTab: boolean = runnerMetrics.length == 0 + + interface TabShowHideDisplay { + hide: boolean, + key: string + } + + const showHideTabs: TabShowHideDisplay[] = [ + { key: "BazelInvocationTabs-Tests", hide: hideTestsTab }, + { key: "BazelInvocationTabs-Targets", hide: hideTargetsTab }, + { key: "BazelInvocationTabs-Network", hide: hideNetworkTab }, + { key: "BazelInvocationTabs-SourceControl", hide: hideSourceControlTab }, + { key: "BazelInvocationTabs-Logs", hide: hideLogsTab }, + { key: "BazelInvocationTabs-Memory", hide: hideMemoryTab }, + { key: "BazelInvocationTabs-Problems", hide: hideProblemsTab }, + { key: "BazelInvocationTabs-Artifacts", hide: hideArtifactsTab }, + { key: "BazelInvocationTabs-ActionsData", hide: hideActionsDataTab }, + { key: "BazelInvocationTabs-Runners", hide: hideRunnersTab }, + ] + + const [activeKey, setActiveKey] = useState(localStorage.getItem("bazelInvocationViewActiveTabKey") ?? 'BazelInvocationTabs-Overview'); + function checkIfNotHidden(key: string) { + var hidden: boolean = showHideTabs.filter(x => x.key == key).at(0)?.hide ?? false + return hidden ? 'BazelInvocationTabs-Overview' : key + } + const onTabChange = (key: string) => { + setActiveKey(key); + localStorage.setItem("bazelInvocationViewActiveTabKey", key) + }; + //tabs var items: TabsProps['items'] = [ { - key: 'BazelInvocationTabs-Problems', - label: 'Problems', - icon: , + key: 'BazelInvocationTabs-Overview', + label: 'Overview', + icon: , children: - {debugMode() && } - {exitCode === null || exitCode.code !== 0 ? ( - children - ) : ( - - )} + } titleBits={["Invocation Overview"]}> + + , }, { @@ -246,29 +297,17 @@ const BazelInvocation: React.FC<{ , }, + { + key: 'BazelInvocationTabs-Problems', + label: 'Problems', + icon: , + children: + + , + }, ]; //show/hide tabs - interface TabShowHideDisplay { - hide: boolean, - key: string - } - - const hideTestsTab: boolean = (testCollection?.length ?? 0) == 0 - const hideTargetsTab: boolean = (targetData?.length ?? 0) == 0 ? true : false - const hideNetworkTab: boolean = bytesRecv == 0 && bytesSent == 0 - const hideSourceControlTab: boolean = sourceControl?.runID == undefined || sourceControl.runID == null || sourceControl.runID == "" ? true : false - const hideLogsTab: boolean = true - const hideMemoryTab: boolean = (memoryMetrics?.peakPostGcHeapSize ?? 0) == 0 && (memoryMetrics?.peakPostGcHeapSize ?? 0) == 0 && (memoryMetrics?.usedHeapSizePostBuild ?? 0) == 0 - - const showHideTabs: TabShowHideDisplay[] = [ - { key: "BazelInvocationTabs-Tests", hide: hideTestsTab }, - { key: "BazelInvocationTabs-Targets", hide: hideTargetsTab }, - { key: "BazelInvocationTabs-Network", hide: hideNetworkTab }, - { key: "BazelInvocationTabs-SourceControl", hide: hideSourceControlTab }, - { key: "BazelInvocationTabs-Logs", hide: hideLogsTab }, - { key: "BazelInvocationTabs-Memory", hide: hideMemoryTab }, - ] for (var i in showHideTabs) { var tab = showHideTabs[i] @@ -291,19 +330,13 @@ const BazelInvocation: React.FC<{ ); } - if (problems?.length) { - extraBits.push( - problem.label).join(' ')} /> - ); - } - if (!isNestedWithinBuildCard && build?.buildUUID) { extraBits.unshift(Build {build.buildUUID}); } return ( } titleBits={titleBits} extraBits={extraBits}> - + ); }; diff --git a/frontend/src/components/BazelInvocationsTable/Columns.tsx b/frontend/src/components/BazelInvocationsTable/Columns.tsx index 390a28b..df98ec1 100644 --- a/frontend/src/components/BazelInvocationsTable/Columns.tsx +++ b/frontend/src/components/BazelInvocationsTable/Columns.tsx @@ -29,7 +29,6 @@ const startedAtColumn: ColumnType = { width: 165, title: 'Start Time', sorter: (a, b) => dayjs(a.startedAt).isBefore(dayjs(b.startedAt)) == true ? 0 : 1, - defaultSortOrder: "descend", render: (_, record) => ( {dayjs(record.startedAt).format('YYYY-MM-DD hh:mm:ss A')} diff --git a/frontend/src/components/Build/index.tsx b/frontend/src/components/Build/index.tsx index ee90e9e..61c5cf7 100644 --- a/frontend/src/components/Build/index.tsx +++ b/frontend/src/components/Build/index.tsx @@ -1,19 +1,19 @@ import React from 'react'; import linkifyHtml from 'linkify-html'; -import {Descriptions, Space, Typography} from 'antd'; +import { Descriptions, Space, Typography } from 'antd'; import themeStyles from '@/theme/theme.module.css'; -import {FindBuildByUuidQuery} from '@/graphql/__generated__/graphql'; +import { FindBuildByUuidQuery } from '@/graphql/__generated__/graphql'; import PortalCard from '@/components/PortalCard'; import PortalAlert from '@/components/PortalAlert'; import BuildStepStatusIcon from '@/components/BuildStepStatusIcon'; -import {getFragmentData} from '@/graphql/__generated__'; +import { getFragmentData } from '@/graphql/__generated__'; import { BAZEL_INVOCATION_FRAGMENT, FULL_BAZEL_INVOCATION_DETAILS, PROBLEM_INFO_FRAGMENT } from "@/app/bazel-invocations/[invocationID]/index.graphql"; import byResultRank from "@/components/Build/index.helpers"; -import {maxBy} from "lodash"; -import {BuildStepResultEnum} from "@/components/BuildStepResultTag"; +import { maxBy } from "lodash"; +import { BuildStepResultEnum } from "@/components/BuildStepResultTag"; import BazelInvocation from "@/components/BazelInvocation"; import BuildProblems from "@/components/Problems"; @@ -67,16 +67,16 @@ const Build: React.FC = ({ buildQueryResults, buildStepToDisplayID, inner { invocations?.map(invocation => { const invocationOverview = getFragmentData(BAZEL_INVOCATION_FRAGMENT, invocation) - const problems = invocation.problems.map(p => getFragmentData(PROBLEM_INFO_FRAGMENT, p)) + //const problems = invocation.problems.map(p => getFragmentData(PROBLEM_INFO_FRAGMENT, p)) return ( ); diff --git a/frontend/src/components/DebugInfo/index.tsx b/frontend/src/components/DebugInfo/index.tsx index ae7ad6e..90814d9 100644 --- a/frontend/src/components/DebugInfo/index.tsx +++ b/frontend/src/components/DebugInfo/index.tsx @@ -10,6 +10,7 @@ interface Props { } const DebugInfo: React.FC = ({ invocationId, exitCode }) => { + return ( ; + +export const InvocationOverviewDisplay: React.FC = ({ targets, command, cpu, user, status, invocationId, configuration, startedAt, endedAt, numFetches, ...props }) => { + return ( + + + + + + + {invocationId} + + + + + + {user} + + + {command} + + + {cpu} + + + {configuration} + + + {numFetches} + + + + ); +}; + +export default InvocationOverviewDisplay; diff --git a/frontend/src/components/NullableBooleanTag/index.tsx b/frontend/src/components/NullableBooleanTag/index.tsx index d829cb9..9121981 100644 --- a/frontend/src/components/NullableBooleanTag/index.tsx +++ b/frontend/src/components/NullableBooleanTag/index.tsx @@ -5,11 +5,14 @@ import themeStyles from '@/theme/theme.module.css'; interface Props { status: boolean | null; + hideText?: boolean } export const BOOL_MAP = [ "true_tag", "false_tag", - "null_tag" + "false_hide_tag", + "null_tag", + "true_hide_tag" ] as const; export type BoolTuple = typeof BOOL_MAP; @@ -18,26 +21,34 @@ export type NilBoolEnum = BoolTuple[number] const BOOL_TAGS: { [key in NilBoolEnum]: React.ReactNode } = { false_tag: ( } color="red" className={themeStyles.tag}>No - ), + false_hide_tag: (} color="red" className={themeStyles.tag} />), true_tag: ( } color="green" className={themeStyles.tag}>Yes - ), + true_hide_tag: (} color="green" className={themeStyles.tag} />), null_tag: ( } color="orange" className={themeStyles.tag}>? - ), }; -const NullBooleanTag: React.FC = ({ status }) => { +const NullBooleanTag: React.FC = ({ status, hideText }) => { + if (hideText == null) { + hideText = false + } var status_string: NilBoolEnum = "null_tag"; - if (status == true) { + if (status == true && hideText == false) { status_string = "true_tag" } - if (status == false) { + if (status == true && hideText == true) { + status_string = "true_hide_tag" + } + if (status == false && hideText == false) { status_string = "false_tag" } + if (status == false && hideText == true) { + status_string = "false_hide_tag" + } const resultTag = BOOL_TAGS[status_string] || BOOL_TAGS.null_tag; return <>{resultTag}; }; diff --git a/frontend/src/components/Problems/index.tsx b/frontend/src/components/Problems/index.tsx index d500d3c..95b62d9 100644 --- a/frontend/src/components/Problems/index.tsx +++ b/frontend/src/components/Problems/index.tsx @@ -1,30 +1,54 @@ /* eslint-disable react/no-array-index-key */ -import React from 'react'; -import { Collapse, CollapseProps } from 'antd'; +import React, { useState } from 'react'; +import { Button, Collapse, CollapseProps } from 'antd'; import CopyTextButton from '@/components/CopyTextButton'; import PortalAlert from '@/components/PortalAlert'; import themeStyles from '@/theme/theme.module.css'; -import {ProblemInfoFragment} from "@/graphql/__generated__/graphql"; -import BuildProblem, {BuildProblemLabel} from "@/components/Problems/BuildProblem"; +import { ProblemInfoFragment } from "@/graphql/__generated__/graphql"; +import BuildProblem, { BuildProblemLabel } from "@/components/Problems/BuildProblem"; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { useQuery } from '@apollo/client'; +import { GET_PROBLEM_DETAILS, PROBLEM_DETAILS_FRAGMENT, PROBLEM_INFO_FRAGMENT } from '@/app/bazel-invocations/[invocationID]/index.graphql'; +import { getFragmentData } from '@/graphql/__generated__'; +import { domainToASCII } from 'url'; interface Props { - problems?: ProblemInfoFragment[]; + invocationId: string; + onTabChange: any; + //problems?: ProblemInfoFragment[]; } + export const CopyAllProblemLabels: React.FC<{ problems: ProblemInfoFragment[] }> = ({ problems }) => { // NOTE: Simplified since ProgressProblem has an '' label. return problem.label).join(' ')} />; }; -const BuildProblems: React.FC = ({ problems }) => { - if (!problems || problems?.length === 0) { - return ( - - ); +const BuildProblems: React.FC = ({ invocationId, onTabChange }) => { + + var { loading, data, previousData, error } = useQuery(GET_PROBLEM_DETAILS, { + variables: { + invocationID: invocationId + }, fetchPolicy: 'cache-and-network' + }); + + var activeData = loading ? previousData : data; + var problems: ProblemInfoFragment[] | undefined = [] + + if (error) { + problems = [] + } else { + const invocation = getFragmentData(PROBLEM_DETAILS_FRAGMENT, activeData?.bazelInvocation) + problems = invocation?.problems.map(p => getFragmentData(PROBLEM_INFO_FRAGMENT, p)) + if (!problems || problems?.length === 0) { + return ( + + ); + } } const progressID = problems.find(problem => problem.__typename === 'ProgressProblem'); diff --git a/frontend/src/components/TargetMetrics/index.tsx b/frontend/src/components/TargetMetrics/index.tsx index cd369bf..3a90c19 100644 --- a/frontend/src/components/TargetMetrics/index.tsx +++ b/frontend/src/components/TargetMetrics/index.tsx @@ -10,6 +10,7 @@ import NullBooleanTag from "../NullableBooleanTag"; import TargetAbortReasonTag, { AbortReasonsEnum } from "./targetAbortReasonTag"; import styles from "../../theme/theme.module.css" import { millisecondsToTime } from "../Utilities/time"; +import Link from "next/link"; interface TargetDataType { key: React.Key; name: string; //label @@ -69,9 +70,10 @@ const TargetMetricsDisplay: React.FC<{ const target_columns: TableColumnsType = [ { - title: "Mnemonic", + title: "Label", dataIndex: "name", filterSearch: true, + render: (_, record) => {record.name}, filterDropdown: filterProps => ( ), diff --git a/frontend/src/components/Targets/TargetDetails/graphql.ts b/frontend/src/components/Targets/TargetDetails/graphql.ts new file mode 100644 index 0000000..ed206e7 --- /dev/null +++ b/frontend/src/components/Targets/TargetDetails/graphql.ts @@ -0,0 +1,34 @@ +import { gql } from '@/graphql/__generated__'; + +export const FIND_TESTS_WITH_CACHE = gql(/* GraphQL */ ` + query FindTestsWithCache( + $first: Int! + $where: TestCollectionWhereInput + $orderBy: TestCollectionOrder + $after: Cursor + ){ + findTests (first: $first, where: $where, orderBy: $orderBy, after: $after){ + totalCount + pageInfo{ + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + node { + id + durationMs + firstSeen + label + overallStatus + cachedLocally + cachedRemotely + bazelInvocation { + invocationID + } + } + } + } + } + `); \ No newline at end of file diff --git a/frontend/src/components/Targets/TargetDetails/index.tsx b/frontend/src/components/Targets/TargetDetails/index.tsx new file mode 100644 index 0000000..cd5e95c --- /dev/null +++ b/frontend/src/components/Targets/TargetDetails/index.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { Space, Row, Statistic } from 'antd'; +import { TestStatusEnum } from '../../TestStatusTag'; +import type { StatisticProps } from "antd/lib"; +import CountUp from 'react-countup'; +import { useQuery } from '@apollo/client'; +import { FindTargetsQueryVariables } from '@/graphql/__generated__/graphql'; +import PortalAlert from '../../PortalAlert'; +import { AreaChart, Area, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts'; +import PortalCard from '../../PortalCard'; +import { FieldTimeOutlined, BorderInnerOutlined } from '@ant-design/icons/lib/icons'; +import TargetGridRow from '../TargetGridRow'; +import { FIND_TARGETS } from '@/app/targets/graphql'; + +interface Props { + label: string +} + +const formatter: StatisticProps['formatter'] = (value) => ( + +); + +export interface TargetStatusType { + label: string + invocationId: string, + status: TestStatusEnum +} +const PAGE_SIZE = 10 +interface GraphDataPoint { + name: string + duration: number +} + + +const TestDetails: React.FC = ({ label }) => { + + const [variables, setVariables] = useState({ first: 1000, where: { label: label } }) + const { loading: labelLoading, data: labelData, previousData: labelPreviousData, error: labelError } = useQuery(FIND_TARGETS, { + variables: variables, + fetchPolicy: 'network-only', + //pollInterval: 120000, + }); + + + const data = labelLoading ? labelPreviousData : labelData; + var result: GraphDataPoint[] = [] + var totalCnt: number = 0 + var total_duration: number = 0 + + if (labelError) { + + } else { + totalCnt = data?.findTargets.totalCount ?? 0 + data?.findTargets.edges?.map(edge => { + var row = edge?.node + result.push({ + name: row?.bazelInvocation?.invocationID ?? "", + duration: row?.durationInMs ?? 0, + }) + total_duration += row?.durationInMs ?? 0 + }); + } + + return ( + +

{label}

+ + + + + + + } titleBits={["Target Duration Over Time"]} > + + + + + + + + + + + + + + + + } titleBits={["Target Pass/Fail Grid"]}> + + + +
+ ); +} +export default TestDetails \ No newline at end of file diff --git a/frontend/src/components/Targets/TargetGrid/index.tsx b/frontend/src/components/Targets/TargetGrid/index.tsx new file mode 100644 index 0000000..efc4bb0 --- /dev/null +++ b/frontend/src/components/Targets/TargetGrid/index.tsx @@ -0,0 +1,167 @@ +import React, { useCallback, useState } from 'react'; +import { TableColumnsType } from "antd/lib" +import { Space, Row, Statistic, Table, TableProps, TablePaginationConfig, Pagination } from 'antd'; +import { TestStatusEnum } from '../../TestStatusTag'; +import type { StatisticProps } from "antd/lib"; +import CountUp from 'react-countup'; +import { SearchFilterIcon, SearchWidget } from '@/components/SearchWidgets'; +import { SearchOutlined } from '@ant-design/icons'; +import { useQuery } from '@apollo/client'; +import { FilterValue } from 'antd/es/table/interface'; +import { uniqueId } from 'lodash'; +import { GetTargetsWithOffsetQueryVariables } from '@/graphql/__generated__/graphql'; +import TargetGridRow from '../TargetGridRow'; +import PortalAlert from '../../PortalAlert'; +import Link from 'next/link'; +import styles from "../../../theme/theme.module.css" +import { millisecondsToTime } from '../../Utilities/time'; +import GET_TARGETS_DATA from '@/app/targets/graphql'; + +interface Props { } + +export interface TargetStatusType { + label: string + invocationId: string, + status: TestStatusEnum +} + +interface TargetGridRowDataType { + key: React.Key; + label: string; + average_duration: number; + min_duration: number; + max_duration: number; + total_count: number; + pass_rate: number; + status: TargetStatusType[]; +} +const formatter: StatisticProps['formatter'] = (value) => ( + +); +const PAGE_SIZE = 10 +const columns: TableColumnsType = [ + { + title: "Label", + dataIndex: "label", + filterSearch: true, + render: (_, record) => + + {record.label}, + filterDropdown: filterProps => ( + + ), + filterIcon: filtered => } filtered={filtered} />, + onFilter: (value, record) => (record.label.includes(value.toString()) ? true : false) + }, + { + title: "Average Duration", + dataIndex: "average_duration", + //sorter: (a, b) => a.average_duration - b.average_duration, + render: (_, record) => {millisecondsToTime(record.average_duration)} + }, + { + title: "Min Duration", + dataIndex: "min_duration", + //sorter: (a, b) => a.average_duration - b.average_duration, + render: (_, record) => {millisecondsToTime(record.min_duration)} + }, + { + title: "Max Duration", + dataIndex: "max_duration", + //sorter: (a, b) => a.average_duration - b.average_duration, + render: (_, record) => {millisecondsToTime(record.max_duration)} + }, + { + title: "# Runs", + dataIndex: "total_count", + align: "right", + render: (_, record) => {record.total_count}, + //sorter: (a, b) => a.total_count - b.total_count, + }, + { + title: "Pass Rate", + dataIndex: "pass_rate", + //sorter: (a, b) => a.pass_rate - b.pass_rate, + render: (_, record) => {(record.pass_rate * 100).toFixed(2)}% + } +] + +const TargetGrid: React.FC = () => { + + const [variables, setVariables] = useState({}) + + const { loading: labelLoading, data: labelData, previousData: labelPreviousData, error: labelError } = useQuery(GET_TARGETS_DATA, { + variables: variables, + fetchPolicy: 'cache-and-network', + }); + + const data = labelLoading ? labelPreviousData : labelData; + var result: TargetGridRowDataType[] = [] + var totalCnt: number = 0 + + if (labelError) { + + } else { + totalCnt = data?.getTargetsWithOffset?.total ?? 0 + data?.getTargetsWithOffset?.result?.map(dataRow => { + var row: TargetGridRowDataType = { + key: "target-grid-row-data-" + uniqueId(), + label: dataRow?.label ?? "", + status: [], + average_duration: dataRow?.avg ?? 0, + min_duration: dataRow?.min ?? 0, + max_duration: dataRow?.max ?? 0, + total_count: dataRow?.count ?? 0, + pass_rate: dataRow?.passRate ?? 0 + } + result.push(row) + }) + } + const onChange: TableProps['onChange'] = useCallback( + (pagination: TablePaginationConfig, + filters: Record, extra: any) => { + var vars: GetTargetsWithOffsetQueryVariables = {} + if (filters['label']?.length) { + var label = filters['label']?.[0]?.toString() ?? "" + vars.label = label + } else { + vars.label = "" + } + vars.offset = ((pagination.current ?? 1) - 1) * PAGE_SIZE; + setVariables(vars) + }, + [variables], + ); + return ( + + + + + + + + + + columns={columns} + loading={labelLoading} + rowKey="key" + onChange={onChange} + expandable={{ + indentSize: 100, + expandedRowRender: (record) => ( + //TODO: dynamically determine number of buttons to display based on page width and pass that as first + + ), + rowExpandable: (_) => true, + }} + pagination={{ + total: totalCnt, + showSizeChanger: false, + }} + dataSource={result} /> + + + ); +}; + +export default TargetGrid; \ No newline at end of file diff --git a/frontend/src/components/Targets/TargetGridBtn/index.tsx b/frontend/src/components/Targets/TargetGridBtn/index.tsx new file mode 100644 index 0000000..fd05555 --- /dev/null +++ b/frontend/src/components/Targets/TargetGridBtn/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + CheckCircleFilled, + CloseCircleFilled, + QuestionCircleFilled, +} from '@ant-design/icons'; +import { Button } from 'antd'; +import themeStyles from '@/theme/theme.module.css'; + +interface Props { + status: boolean | null; + invocationId: string; +} + +function getIconForStatus(status: boolean | null) { + if (status == null) { + return + } + if (status == true) { + return + } + return +} + +function getClassForStatus(status: boolean | null) { + if (status == null) { + return themeStyles.colorDisabled + } + if (status == true) { + return themeStyles.colorSuccess + } + return themeStyles.colorFailure +} + +const TargetGridBtn: React.FC = ({ status, invocationId }) => { + const resultTag =