From e0ac37aaf63758e77a957edf79026256be23c519 Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Mon, 25 Apr 2022 15:02:05 -0500 Subject: [PATCH 001/102] WIP: display latest runs --- packages/app/src/specs/SpecsList.vue | 34 ++++++++++++++++++++++--- packages/graphql/schemas/cloud.graphql | 12 +++++++++ packages/graphql/schemas/schema.graphql | 10 ++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index 16b0e2d40aa6..a88b4cd7a13f 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -115,6 +115,7 @@ class="mt-56px" @clear="handleClear" /> +
{{JSON.stringify(specs)}}
@@ -172,6 +173,20 @@ fragment SpecsList on Spec { } ` + +gql` +fragment CloudSpecs_123 on CloudProject{ + specs(specPaths: ["cypress/e2e/practice/practice.cy.js"]) { + specPath + averageDuration + recentRuns { + id + status + } + } +} +` + gql` fragment Specs_SpecsList on Query { currentProject { @@ -184,6 +199,9 @@ fragment Specs_SpecsList on Query { } config ...SpecPatternModal + cloudProject{ + ...CloudSpecs_123 + } } } ` @@ -229,13 +247,23 @@ function handleClear () { } const specs = computed(() => { - const specs = cachedSpecs.value.map((x) => makeFuzzyFoundSpec(x)) + const specs2 = cachedSpecs.value.map((x) => { + const s = makeFuzzyFoundSpec(x) + // console.log(JSON.stringify(props.gql.currentProject.cloudProject)) + // console.log(s) + const runInfo = props.gql.currentProject.cloudProject.specs.find(ss=>ss.specPath === s.name) + // console.log(runInfo) + return { + ...s, + runInfo + } + }) if (!debouncedSearchString.value) { - return specs + return specs2 } - return fuzzySortSpecs(specs, debouncedSearchString.value) + return fuzzySortSpecs(specs2, debouncedSearchString.value) }) const collapsible = computed(() => { diff --git a/packages/graphql/schemas/cloud.graphql b/packages/graphql/schemas/cloud.graphql index 5bba7a93c46d..6167acad79df 100644 --- a/packages/graphql/schemas/cloud.graphql +++ b/packages/graphql/schemas/cloud.graphql @@ -140,6 +140,11 @@ type CloudProject implements Node { Unique identifier for a Project """ slug: String! + + """ + List of specs in the project. + """ + specs(specPaths: [String!]): [CloudProjectSpecs] } type CloudProjectConnection { @@ -186,6 +191,13 @@ union CloudProjectResult = | CloudProjectNotFound | CloudProjectUnauthorized +type CloudProjectSpecs { + averageDuration: Float + recentRuns: [CloudRun] + specHash: String + specPath: String +} + """ Unauthorized access """ diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index d44cedaea7d3..f74c0003511f 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -156,6 +156,9 @@ type CloudProject implements Node { """Unique identifier for a Project""" slug: String! + + """List of specs in the project.""" + specs(specPaths: [String!]): [CloudProjectSpecs] } type CloudProjectConnection { @@ -189,6 +192,13 @@ type CloudProjectNotFound { union CloudProjectResult = CloudProject | CloudProjectNotFound | CloudProjectUnauthorized +type CloudProjectSpecs { + averageDuration: Float + recentRuns: [CloudRun] + specHash: String + specPath: String +} + """Unauthorized access""" type CloudProjectUnauthorized { """does the user have a requested access pending""" From a595cbf89cd3c7c42a875874efca969ee7885461 Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Thu, 28 Apr 2022 15:48:00 -0500 Subject: [PATCH 002/102] WIP: display latest runs [CLOUD-577] --- packages/app/src/specs/RunStatusDots.vue | 98 +++++++++++++++++++ packages/app/src/specs/SpecsList.vue | 36 ++++--- packages/app/src/specs/SpecsListRowItem.vue | 6 +- .../src/assets/icons/cancelled-solid_x16.svg | 3 + .../src/assets/icons/dot-solid_x4.svg | 3 + .../src/assets/icons/errored-solid_x16.svg | 3 + .../src/assets/icons/failed-solid_x16.svg | 4 + .../src/assets/icons/passed-solid_x16.svg | 4 + .../assets/icons/placeholder-solid_x16.svg | 3 + .../src/assets/icons/running-outline_x16.svg | 4 + .../frontend-shared/src/locales/en-US.json | 1 + 11 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 packages/app/src/specs/RunStatusDots.vue create mode 100644 packages/frontend-shared/src/assets/icons/cancelled-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/dot-solid_x4.svg create mode 100644 packages/frontend-shared/src/assets/icons/errored-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/failed-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/passed-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/placeholder-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/running-outline_x16.svg diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue new file mode 100644 index 000000000000..3c27b23dae2b --- /dev/null +++ b/packages/app/src/specs/RunStatusDots.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index a88b4cd7a13f..70c6b8780e3b 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -32,7 +32,7 @@ />
{{ t('specPage.gitStatusHeader') }}
+
+
{{ t('specPage.latestRunsHeader') }}
+
+ + + diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index f7e2a61062fb..df69bf6b9b60 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -127,9 +127,11 @@ fragment RunStatusDots on Spec { const props = withDefaults(defineProps<{ gql: RunStatusDotsFragment | null specFile: string|null + isProjectDisconnected: boolean }>(), { runs: () => [], specFile: null, + isProjectDisconnected: false, }) const runs = computed(() => { @@ -139,7 +141,7 @@ const runs = computed(() => { const isLoading = computed(() => { const loadingStatuses: RemoteFetchableStatus[] = ['FETCHING', 'NOT_FETCHED'] - return loadingStatuses.some((s) => s === props.gql?.cloudSpec?.status) + return !props.isProjectDisconnected && loadingStatuses.some((s) => s === props.gql?.cloudSpec?.status) }) const dotClasses = computed(() => { diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index 7798c925cc0f..ab39fd064050 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -33,7 +33,7 @@ />
+
+ +
diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index df69bf6b9b60..ec97b87e7b2e 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -1,7 +1,7 @@ +
+
+
    +
  1. + +
  2. +
  3. + + + +
  4. +
+
+
import ExternalLink from '@cy/gql-components/ExternalLink.vue' -import type { RemoteFetchableStatus, RunStatusDotsFragment } from '../generated/graphql' +import { /*RemoteFetchableStatus,*/ RunStatusDotsFragment, RunStatusDots_RefetchDocument } from '../generated/graphql' import Tooltip, { InteractiveHighlightColor } from '@packages/frontend-shared/src/components/Tooltip.vue' import { computed, ComputedRef } from 'vue' import CancelledIcon from '~icons/cy/cancelled-solid_x16.svg' @@ -78,7 +109,7 @@ import PassedIcon from '~icons/cy/passed-solid_x16.svg' import PlaceholderIcon from '~icons/cy/placeholder-solid_x16.svg' import RunningIcon from '~icons/cy/running-outline_x16.svg' import SpecRunSummary from './SpecRunSummary.vue' -import { gql } from '@urql/vue' +import { gql, useMutation } from '@urql/vue' // type Maybe = T | null; gql` @@ -138,11 +169,11 @@ const runs = computed(() => { return props.gql?.cloudSpec?.data?.specRuns?.nodes ?? [] }) -const isLoading = computed(() => { - const loadingStatuses: RemoteFetchableStatus[] = ['FETCHING', 'NOT_FETCHED'] +// const isLoading = computed(() => { +// const loadingStatuses: RemoteFetchableStatus[] = ['FETCHING', 'NOT_FETCHED'] - return !props.isProjectDisconnected && loadingStatuses.some((s) => s === props.gql?.cloudSpec?.status) -}) +// return !props.isProjectDisconnected && loadingStatuses.some((s) => s === props.gql?.cloudSpec?.status) +// }) const dotClasses = computed(() => { const statuses = ['placeholder', 'placeholder', 'placeholder'] @@ -234,6 +265,23 @@ const latestRunColor: ComputedRef = computed(() => { } }) +gql` +mutation RunStatusDots_Refetch ($ids: [ID!]!) { + loadRemoteFetchables(ids: $ids){ + id + status + } +} +` + +const refetchMutation = useMutation(RunStatusDots_RefetchDocument) + +const refetch = () => { + if (props.gql?.cloudSpec?.id && !refetchMutation.fetching.value) { + refetchMutation.executeMutation({ ids: [props.gql?.cloudSpec?.id] }) + } +} + diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index c442827aa129..716091a1b3de 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -51,6 +51,8 @@ :header-text="t('specPage.latestRuns.header')" :connected-text="t('specPage.latestRuns.tooltip.connected')" :not-connected-text="t('specPage.latestRuns.tooltip.notConnected')" + @showLogin="showLogin" + @showConnectToProject="showConnectToProject" />
@@ -59,6 +61,8 @@ :header-text="t('specPage.averageDuration.header')" :connected-text="t('specPage.averageDuration.tooltip.connected')" :not-connected-text="t('specPage.averageDuration.tooltip.notConnected')" + @showLogin="showLogin" + @showConnectToProject="showConnectToProject" />
@@ -128,8 +132,8 @@
{{ JSON.stringify(row.data.data?.cloudSpec?.data?.specRuns?.nodes?.length ?? "null",null,2) }}
--> @@ -152,10 +156,23 @@ @clear="handleClear" /> + + diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index 5aa1610afed2..03f608913b9f 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -13,6 +13,7 @@
    -
    +
    {{ headerText }}
    @@ -111,9 +106,10 @@ const theme = computed(() => { } } -.v-popper__popper.v-popper--theme-interactive { +.v-popper__popper.v-popper--theme-menu { .v-popper__inner { - @apply bg-white border-dark-900 text-black; + @apply bg-white text-black; + border-color: transparent; border-radius: 4px !important; box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.15); padding: 0; diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 32cfdc327d6a..481dea0f2f24 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -121,7 +121,13 @@ "componentSpecsHeader": "Component specs", "e2eSpecsHeader": "E2E specs", "lastUpdated": { - "header": "Last updated" + "header": "Last updated", + "tooltip": { + "gitStatus": "Git status", + "gitInfo": "git info", + "gitInfoAvailable": "{0} of the spec files within this project", + "gitInfoUnavailable": "Cypress is unable to detect {0} for this project and has defaulted to showing file system data instead" + } }, "latestRuns": { "header": "Latest runs", From 92c2babe8c9c961989ae847174663a069c2f94eb Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Fri, 3 Jun 2022 08:25:42 -0500 Subject: [PATCH 022/102] Fixed a few broken tests --- packages/app/src/runs/RunCard.vue | 2 +- packages/app/src/runs/RunResults.vue | 7 ++++++- packages/frontend-shared/src/components/ResultCounts.vue | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/app/src/runs/RunCard.vue b/packages/app/src/runs/RunCard.vue index a946a9b60f87..7afae3eccd60 100644 --- a/packages/app/src/runs/RunCard.vue +++ b/packages/app/src/runs/RunCard.vue @@ -57,7 +57,7 @@ diff --git a/packages/app/src/runs/RunResults.vue b/packages/app/src/runs/RunResults.vue index 10aeccb363c2..fe79447f28f5 100644 --- a/packages/app/src/runs/RunResults.vue +++ b/packages/app/src/runs/RunResults.vue @@ -4,7 +4,12 @@ v-if="props.gql.totalFlakyTests" class="rounded-md font-semibold bg-warning-100 text-sm py-2px px-4px text-warning-600 whitespace-nowrap" >{{ props.gql.totalFlakyTests }} Flaky - +
    diff --git a/packages/frontend-shared/src/components/ResultCounts.vue b/packages/frontend-shared/src/components/ResultCounts.vue index aef4d5b65585..64ee16a8d972 100644 --- a/packages/frontend-shared/src/components/ResultCounts.vue +++ b/packages/frontend-shared/src/components/ResultCounts.vue @@ -9,7 +9,7 @@ > {{ result.name }} @@ -20,7 +20,6 @@ diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index 03f608913b9f..953433dc3baa 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -1,23 +1,22 @@ @@ -159,17 +196,18 @@ diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index 22a865de15bb..f655f5e3b619 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -70,6 +70,7 @@
    collapsible.value.tree.filter(((item) => !it const { containerProps, list, wrapperProps, scrollTo } = useVirtualList(treeSpecList, { itemHeight: 40, overscan: 10 }) +const scrollbarOffset = ref(0) + +// Watch the sizing of the specs list so we can detect when a scrollbar is added/removed +// We then calculate the width of the scrollbar and add that as padding to the list header +// so that the columns stay aligned +useResizeObserver(containerProps.ref, (entries) => { + const specListContainer = entries?.[0] + const containerElement = specListContainer?.target as HTMLElement + + if (containerElement) { + const displayedScrollbarWidth = containerElement.offsetWidth - containerElement.clientWidth + + scrollbarOffset.value = displayedScrollbarWidth + } else { + scrollbarOffset.value = 0 + } +}) + // If you are scrolled down the virtual list and list changes, // reset scroll position to top of list watch(() => treeSpecList.value, () => scrollTo(0)) diff --git a/packages/frontend-shared/src/components/ResultCounts.vue b/packages/frontend-shared/src/components/ResultCounts.vue index 64ee16a8d972..bbe5a211657e 100644 --- a/packages/frontend-shared/src/components/ResultCounts.vue +++ b/packages/frontend-shared/src/components/ResultCounts.vue @@ -3,7 +3,7 @@
    diff --git a/packages/frontend-shared/src/components/Tooltip.vue b/packages/frontend-shared/src/components/Tooltip.vue index b500957df6b1..e2d87a999b7d 100644 --- a/packages/frontend-shared/src/components/Tooltip.vue +++ b/packages/frontend-shared/src/components/Tooltip.vue @@ -2,7 +2,7 @@ (), { color: 'dark', hideArrow: false, @@ -42,6 +43,7 @@ const props = withDefaults(defineProps<{ distance: undefined, skidding: undefined, placement: undefined, + popperClass: undefined, }) const theme = computed(() => { From c322c71ea728880f8981b5d68ee1f3f0ee4b623d Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Thu, 9 Jun 2022 14:45:24 -0500 Subject: [PATCH 030/102] Fixed styling for RunStatusDots and skeleton --- packages/app/src/specs/RunStatusDots.vue | 60 +++++++++++------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index a86b19983b65..f3b0cf410b57 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -1,10 +1,10 @@ From e2ca7788407245ca465b44c8a18353ac7bf05476 Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Thu, 9 Jun 2022 14:57:49 -0500 Subject: [PATCH 031/102] Fixed TS issues --- packages/app/src/specs/AverageDuration.cy.tsx | 5 +++-- packages/app/src/specs/RunStatusDots.cy.tsx | 20 +++++++++++-------- .../src/sources/RemotePollingDataSource.ts | 4 ++-- .../support/mock-graphql/stubgql-Mutation.ts | 2 +- .../launchpad/src/setup/OpenBrowserList.vue | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/app/src/specs/AverageDuration.cy.tsx b/packages/app/src/specs/AverageDuration.cy.tsx index e076b565a57d..51e3370f51eb 100644 --- a/packages/app/src/specs/AverageDuration.cy.tsx +++ b/packages/app/src/specs/AverageDuration.cy.tsx @@ -10,10 +10,11 @@ function emptyAverageDurationFragment (milliseconds?: number): AverageDurationFr data: { id: 'id', averageDuration: milliseconds ?? 0, + retrievedAt: new Date().toISOString(), __typename: 'CloudProjectSpec', }, - status: 'FETCHED', - __typename: 'RemoteFetchableCloudProjectSpec', + fetchingStatus: 'FETCHED', + __typename: 'RemoteFetchableCloudProjectSpecResult', }, } } diff --git a/packages/app/src/specs/RunStatusDots.cy.tsx b/packages/app/src/specs/RunStatusDots.cy.tsx index 821bd52f8fa4..dde3f0547b85 100644 --- a/packages/app/src/specs/RunStatusDots.cy.tsx +++ b/packages/app/src/specs/RunStatusDots.cy.tsx @@ -13,11 +13,12 @@ describe('', () => { fileName: 'spec', specFileExtension: '.cy.ts', cloudSpec: { - __typename: 'RemoteFetchableCloudProjectSpec', + __typename: 'RemoteFetchableCloudProjectSpecResult', id: 'id', - status: 'FETCHED', + fetchingStatus: 'FETCHED', data: { __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), id: 'id', specRuns: { nodes: [ @@ -51,12 +52,13 @@ describe('', () => { fileName: 'spec', specFileExtension: '.cy.ts', cloudSpec: { - __typename: 'RemoteFetchableCloudProjectSpec', + __typename: 'RemoteFetchableCloudProjectSpecResult', id: 'id', - status: 'FETCHED', + fetchingStatus: 'FETCHED', data: { __typename: 'CloudProjectSpec', id: 'id', + retrievedAt: new Date().toISOString(), specRuns: { nodes: [ ...runs as any, // suppress TS compiler @@ -89,12 +91,13 @@ describe('', () => { fileName: 'spec', specFileExtension: '.cy.ts', cloudSpec: { - __typename: 'RemoteFetchableCloudProjectSpec', + __typename: 'RemoteFetchableCloudProjectSpecResult', id: 'id', - status: 'FETCHED', + fetchingStatus: 'FETCHED', data: { __typename: 'CloudProjectSpec', id: 'id', + retrievedAt: new Date().toISOString(), specRuns: { nodes: [ ...runs as any, // suppress TS compiler @@ -125,12 +128,13 @@ describe('', () => { fileName: 'spec', specFileExtension: '.cy.ts', cloudSpec: { - __typename: 'RemoteFetchableCloudProjectSpec', + __typename: 'RemoteFetchableCloudProjectSpecResult', id: 'id', - status: 'FETCHED', + fetchingStatus: 'FETCHED', data: { __typename: 'CloudProjectSpec', id: 'id', + retrievedAt: new Date().toISOString(), specRuns: { nodes: [], }, diff --git a/packages/data-context/src/sources/RemotePollingDataSource.ts b/packages/data-context/src/sources/RemotePollingDataSource.ts index 7efb889866b9..47330e2e5fa5 100644 --- a/packages/data-context/src/sources/RemotePollingDataSource.ts +++ b/packages/data-context/src/sources/RemotePollingDataSource.ts @@ -72,8 +72,8 @@ export class RemotePollingDataSource { this.ctx.emitter.specPollingUpdate(mostRecentUpdate) - this.#specPolling = setTimeout(() => { - this.#sendSpecPollingRequest(commitBranch, projectSlug) + this.#specPolling = setTimeout(async () => { + await this.#sendSpecPollingRequest(commitBranch, projectSlug) }, secondsToPollNext * 1000) return result diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts index 19f2be1e7941..7f14a8af91fa 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts @@ -129,7 +129,7 @@ export const stubMutation: MaybeResolver = { const rf: RemoteFetchable & {__typename: string} = { id, - status: 'FETCHING', + fetchingStatus: 'FETCHING', operation: '', operationHash: hash, operationVariables: {}, diff --git a/packages/launchpad/src/setup/OpenBrowserList.vue b/packages/launchpad/src/setup/OpenBrowserList.vue index c148501549e1..e275d69c6d69 100644 --- a/packages/launchpad/src/setup/OpenBrowserList.vue +++ b/packages/launchpad/src/setup/OpenBrowserList.vue @@ -28,7 +28,7 @@ > Date: Thu, 9 Jun 2022 16:31:52 -0500 Subject: [PATCH 032/102] Finished polling implementation --- packages/app/src/pages/Specs/Index.vue | 11 ++- packages/app/src/specs/AverageDuration.vue | 86 ++++++++++++++--- packages/app/src/specs/RunStatusDots.vue | 92 ++++++++++++++++--- packages/app/src/specs/SpecsList.vue | 11 ++- .../src/sources/RemotePollingDataSource.ts | 20 ++-- .../src/components/Tooltip.vue | 14 ++- .../launchpad/src/setup/OpenBrowserList.vue | 2 +- 7 files changed, 186 insertions(+), 50 deletions(-) diff --git a/packages/app/src/pages/Specs/Index.vue b/packages/app/src/pages/Specs/Index.vue index 4c7c2581d68e..25c61879438f 100644 --- a/packages/app/src/pages/Specs/Index.vue +++ b/packages/app/src/pages/Specs/Index.vue @@ -11,6 +11,7 @@ import { computed, ref } from 'vue' -import { gql, useQuery, useSubscription } from '@urql/vue' +import { gql, SubscriptionHandlerArg, useQuery, useSubscription } from '@urql/vue' import { useI18n } from '@cy/i18n' import SpecsList from '../../specs/SpecsList.vue' import NoSpecsPage from '../../specs/NoSpecsPage.vue' @@ -95,10 +96,16 @@ useSubscription({ variables, }) +const mostRecentUpdate = ref(null) + +const updateMostRecentUpdate: SubscriptionHandlerArg = (_, reportedUpdate) => { + mostRecentUpdate.value = reportedUpdate?.startPollingForSpecs ?? null +} + useSubscription({ query: SpecsPageContainer_SpecListPollingDocument, variables: pollingVariables, -}) +}, updateMostRecentUpdate) const query = useQuery({ query: SpecsPageContainerDocument, diff --git a/packages/app/src/specs/AverageDuration.vue b/packages/app/src/specs/AverageDuration.vue index 5f5dd220bb2e..1b7eb0c02ef8 100644 --- a/packages/app/src/specs/AverageDuration.vue +++ b/packages/app/src/specs/AverageDuration.vue @@ -13,7 +13,7 @@ import { AverageDurationFragment, AverageDuration_RefetchDocument } from '../generated/graphql' import { gql, useMutation } from '@urql/vue' import { getDurationString } from '@packages/frontend-shared/src/utils/time' -import { watch, watchEffect } from 'vue' +import { ref, watch } from 'vue' gql` mutation AverageDuration_Refetch ($ids: [ID!]!) { @@ -26,9 +26,14 @@ mutation AverageDuration_Refetch ($ids: [ID!]!) { const refetchMutation = useMutation(AverageDuration_RefetchDocument) -const refetch = () => { +const isRefetching = ref(false) + +const refetch = async () => { if (!props.isProjectDisconnected && props.gql?.avgDurationInfo?.id && !refetchMutation.fetching.value) { - refetchMutation.executeMutation({ ids: [props.gql?.avgDurationInfo?.id] }) + isRefetching.value = true + + await refetchMutation.executeMutation({ ids: [props.gql?.avgDurationInfo?.id] }) + isRefetching.value = false } } @@ -40,6 +45,9 @@ fragment AverageDuration on Spec { id fetchingStatus data { + ... on CloudProjectSpecNotFound { + retrievedAt + } ... on CloudProjectSpec { id averageDuration(fromBranch: $fromBranch) @@ -54,25 +62,75 @@ const props = withDefaults(defineProps<{ gql: AverageDurationFragment | null isProjectDisconnected: boolean isOnline: boolean + mostRecentUpdate: string | null }>(), { gql: null, isProjectDisconnected: false, isOnline: true, + mostRecentUpdate: null, }) -watchEffect( - () => { - if (props.isOnline && (props.gql?.avgDurationInfo?.fetchingStatus === 'NOT_FETCHED' || props.gql?.avgDurationInfo?.fetchingStatus === undefined)) { - refetch() - } - }, -) +function shouldRefetch () { + if (isRefetching.value) { + // refetch in progress, no need to refetch -watch(() => props.isProjectDisconnected, (value, oldValue) => { - if (value && !oldValue) { - refetch() + return false } -}) + + if (!props.isOnline) { + // Offline, no need to refetch + + return false + } + + if (props.gql?.avgDurationInfo?.fetchingStatus === 'NOT_FETCHED' || props.gql?.avgDurationInfo?.fetchingStatus === undefined) { + // NOT_FETCHED, refetch + + return true + } + + if (props.mostRecentUpdate) { + if ( + ( + props.gql?.avgDurationInfo?.data?.__typename === 'CloudProjectSpecNotFound' || + props.gql?.avgDurationInfo?.data?.__typename === 'CloudProjectSpec' + ) + && ( + props.gql.avgDurationInfo.data.retrievedAt && + props.mostRecentUpdate > props.gql.avgDurationInfo.data.retrievedAt + ) + ) { + // outdated, refetch + + return true + } + } + + // nothing new, no need to refetch + + return false +} + +watch(() => props.isOnline, + async () => { + if (shouldRefetch()) { + await refetch() + } + }, { immediate: true }) + +watch(() => props.isProjectDisconnected, + async () => { + if (shouldRefetch()) { + await refetch() + } + }, { immediate: true }) + +watch(() => props.mostRecentUpdate, + async () => { + if (shouldRefetch()) { + await refetch() + } + }, { immediate: true }) diff --git a/packages/app/src/specs/RunStatusDots.vue b/packages/app/src/specs/RunStatusDots.vue index f3b0cf410b57..6fb3677ec09d 100644 --- a/packages/app/src/specs/RunStatusDots.vue +++ b/packages/app/src/specs/RunStatusDots.vue @@ -74,7 +74,7 @@ import ExternalLink from '@cy/gql-components/ExternalLink.vue' import { RunStatusDotsFragment, RunStatusDots_RefetchDocument } from '../generated/graphql' import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue' -import { computed, watch, watchEffect } from 'vue' +import { computed, ref, watch } from 'vue' import CancelledIcon from '~icons/cy/cancelled-solid_x16.svg' import ErroredIcon from '~icons/cy/errored-solid_x16.svg' import FailedIcon from '~icons/cy/failed-solid_x16.svg' @@ -95,9 +95,18 @@ mutation RunStatusDots_Refetch ($ids: [ID!]!) { const refetchMutation = useMutation(RunStatusDots_RefetchDocument) -const refetch = () => { +const isRefetching = ref(false) + +const refetch = async () => { + if (isRefetching.value) { + return + } + if (!props.isProjectDisconnected && props.gql.cloudSpec?.id && !refetchMutation.fetching.value) { - refetchMutation.executeMutation({ ids: [props.gql.cloudSpec?.id] }) + isRefetching.value = true + + await refetchMutation.executeMutation({ ids: [props.gql.cloudSpec.id] }) + isRefetching.value = false } } @@ -111,6 +120,9 @@ fragment RunStatusDots on Spec { fetchingStatus data { __typename + ... on CloudProjectSpecNotFound { + retrievedAt + } ... on CloudProjectSpec { id retrievedAt @@ -154,18 +166,74 @@ const props = withDefaults(defineProps<{ gql: RunStatusDotsFragment isProjectDisconnected: boolean isOnline: boolean + mostRecentUpdate: string | null }>(), { isProjectDisconnected: false, isOnline: true, + mostRecentUpdate: null, }) -watchEffect( - () => { - if (props.isOnline && (props.gql.cloudSpec?.fetchingStatus === 'NOT_FETCHED' || props.gql.cloudSpec?.fetchingStatus === undefined)) { - refetch() +function shouldRefetch () { + if (isRefetching.value) { + // refetch in progress, no need to refetch + + return false + } + + if (!props.isOnline) { + // Offline, no need to refetch + + return false + } + + if (props.gql.cloudSpec?.fetchingStatus === 'NOT_FETCHED' || props.gql.cloudSpec?.fetchingStatus === undefined) { + // NOT_FETCHED, refetch + + return true + } + + if (props.mostRecentUpdate) { + if ( + ( + props.gql.cloudSpec?.data?.__typename === 'CloudProjectSpecNotFound' || + props.gql.cloudSpec?.data?.__typename === 'CloudProjectSpec' + ) + && ( + props.gql.cloudSpec.data.retrievedAt && + props.mostRecentUpdate > props.gql.cloudSpec.data.retrievedAt + ) + ) { + // outdated, refetch + + return true + } + } + + // nothing new, no need to refetch + + return false +} + +watch(() => props.isOnline, + async () => { + if (shouldRefetch()) { + await refetch() } - }, -) + }, { immediate: true }) + +watch(() => props.isProjectDisconnected, + async () => { + if (shouldRefetch()) { + await refetch() + } + }, { immediate: true }) + +watch(() => props.mostRecentUpdate, + async () => { + if (shouldRefetch()) { + await refetch() + } + }, { immediate: true }) const runs = computed(() => { return props.gql.cloudSpec?.data?.__typename === 'CloudProjectSpec' ? props.gql.cloudSpec.data.specRuns?.nodes ?? [] : [] @@ -235,12 +303,6 @@ const latestStatus = computed(() => { } }) -watch(() => props.isProjectDisconnected, (value, oldValue) => { - if (value && !oldValue) { - refetch() - } -}) - diff --git a/packages/app/src/specs/LastUpdatedHeader.vue b/packages/app/src/specs/LastUpdatedHeader.vue index 09f1ac31cbdc..8c6078b57c67 100644 --- a/packages/app/src/specs/LastUpdatedHeader.vue +++ b/packages/app/src/specs/LastUpdatedHeader.vue @@ -2,6 +2,7 @@
    -
    -
    -
    - -
    -
    - -
    +
    -
    - +
    +
    -
    -   -
    -
    + + + + + diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index 0fd875ba0a27..625f57a8e760 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -42,7 +42,17 @@ {{ t('specPage.fetchFailedWarning.explainer1') }}

    - {{ t('specPage.fetchFailedWarning.explainer2') }} + + + Status Page + +

    @@ -212,6 +212,7 @@ isOffline.value = !newIsOnlineValue) const isProjectConnectOpen = ref(false) const isLoginOpen = ref(false) +const loginUtmMedium = ref('') -const showLogin = () => { +const showLogin = (utmMedium: string) => { + loginUtmMedium.value = utmMedium isLoginOpen.value = true } diff --git a/packages/data-context/src/actions/AuthActions.ts b/packages/data-context/src/actions/AuthActions.ts index 7270b05c1bcd..fe7f7da3f899 100644 --- a/packages/data-context/src/actions/AuthActions.ts +++ b/packages/data-context/src/actions/AuthActions.ts @@ -47,7 +47,7 @@ export class AuthActions { } } - async login () { + async login (utmMedium?: string | null) { const onMessage = (authState: AuthStateShape) => { this.ctx.update((coreData) => { coreData.authState = authState @@ -65,7 +65,7 @@ export class AuthActions { this.#cancelActiveLogin = () => resolve(null) // NOTE: auth.logIn should never reject, it uses `onMessage` to propagate state changes (including errors) to the frontend. - this.authApi.logIn(onMessage, 'launchpad').then(resolve, reject) + this.authApi.logIn(onMessage, utmMedium ?? 'launchpad').then(resolve, reject) }) const isMainWindowFocused = this.ctx._apis.electronApi.isMainWindowFocused() diff --git a/packages/frontend-shared/src/gql-components/Auth.vue b/packages/frontend-shared/src/gql-components/Auth.vue index 0f858b1a8cb3..58d19dc9c81b 100644 --- a/packages/frontend-shared/src/gql-components/Auth.vue +++ b/packages/frontend-shared/src/gql-components/Auth.vue @@ -86,6 +86,7 @@ const props = defineProps<{ gql: AuthFragment showRetry?: boolean showLogout?: boolean + utmMedium?: string }>() gql` @@ -112,8 +113,8 @@ mutation Auth_Logout { ` gql` -mutation Auth_Login { - login { +mutation Auth_Login ($utmMedium: String) { + login (utmMedium: $utmMedium) { ...Auth } } @@ -183,7 +184,7 @@ const handleLoginOrContinue = async () => { loginInitiated.value = true - login.executeMutation({}) + login.executeMutation({ utmMedium: props.utmMedium ?? null }) } const handleLogout = () => { @@ -193,7 +194,7 @@ const handleLogout = () => { const handleTryAgain = async () => { await reset.executeMutation({}) - login.executeMutation({}) + login.executeMutation({ utmMedium: props.utmMedium ?? null }) } const handleCancel = () => { diff --git a/packages/frontend-shared/src/gql-components/topnav/LoginModal.vue b/packages/frontend-shared/src/gql-components/topnav/LoginModal.vue index 09f8a0fbe146..4f513803db18 100644 --- a/packages/frontend-shared/src/gql-components/topnav/LoginModal.vue +++ b/packages/frontend-shared/src/gql-components/topnav/LoginModal.vue @@ -79,6 +79,7 @@
    @@ -123,6 +124,7 @@ const emit = defineEmits<{ const props = defineProps<{ modelValue: boolean gql: LoginModalFragment + utmMedium?: string }>() gql` diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 2c8098781a56..1b4a9e5735a2 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -1174,7 +1174,7 @@ type Mutation { ): [RemoteFetchable]! """Auth with Cypress Dashboard""" - login: Query + login(utmMedium: String): Query """Log out of Cypress Dashboard""" logout: Query diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 267244946f2d..63e8832aa563 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -259,8 +259,11 @@ export const mutation = mutationType({ t.field('login', { type: Query, description: 'Auth with Cypress Dashboard', + args: { + utmMedium: stringArg(), + }, resolve: async (_, args, ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login(args.utmMedium) return {} }, From 72f0653bc1d3a1346558d058602849d57f124a75 Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Sun, 12 Jun 2022 21:45:29 -0500 Subject: [PATCH 049/102] Fixed a minor UX is with cloud data fetching --- packages/app/src/specs/SpecsList.vue | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index 9870755f992e..e22903fdf7fe 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -442,20 +442,11 @@ mutation CloudData_Refetch ($ids: [ID!]!) { const refetchMutation = useMutation(CloudData_RefetchDocument) -const isRefetching = ref(false) - const isProjectDisconnected = computed(() => props.gql.cloudViewer?.id === undefined || (props.gql.currentProject?.cloudProject?.__typename !== 'CloudProject')) const refetch = async (ids: string[]) => { - if (isRefetching.value) { - return - } - if (!isProjectDisconnected.value && !refetchMutation.fetching.value) { - isRefetching.value = true - await refetchMutation.executeMutation({ ids }) - isRefetching.value = false } } @@ -473,12 +464,6 @@ type CloudSpecItem = { } function shouldRefetch (item: CloudSpecItem) { - if (isRefetching.value) { - // refetch in progress, no need to refetch - - return false - } - if (!isOnline) { // Offline, no need to refetch From bd1a839aa2373c57e44a8ddc0ea15e800e420aae Mon Sep 17 00:00:00 2001 From: Muaz Othman Date: Sun, 12 Jun 2022 23:43:08 -0500 Subject: [PATCH 050/102] Added missing imports --- packages/app/src/specs/SpecHeaderCloudDataTooltip.vue | 1 + packages/app/src/specs/SpecsList.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue b/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue index b4045e2f940f..923a5056b5e9 100644 --- a/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue +++ b/packages/app/src/specs/SpecHeaderCloudDataTooltip.vue @@ -76,6 +76,7 @@ import Button from '@cy/components/Button.vue' import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue' import ConnectIcon from '~icons/cy/chain-link_x16.svg' import SendIcon from '~icons/cy/paper-airplane_x16.svg' +import ExternalLink from '@cy/gql-components/ExternalLink.vue' import { RunsErrorRenderer_RequestAccessDocument, SpecHeaderCloudDataTooltipFragment } from '../generated/graphql' import { useI18n } from '@cy/i18n' import { computed } from 'vue' diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue index e22903fdf7fe..64af28a427a0 100644 --- a/packages/app/src/specs/SpecsList.vue +++ b/packages/app/src/specs/SpecsList.vue @@ -224,6 +224,7 @@