Skip to content

internal: show loading and error statuses in studio panel, add download timeout #31633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
9 changes: 8 additions & 1 deletion packages/app/cypress/e2e/studio/studio.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ describe('studio functionality', () => {
it('loads the studio page', () => {
launchStudio({ enableCloudStudio: true })

cy.get('[data-cy="loading-studio-panel"]').should('not.exist')

cy.window().then((win) => {
expect(win.Cypress.config('isDefaultProtocolEnabled')).to.be.false
expect(win.Cypress.state('isProtocolEnabled')).to.be.true
})
})

it('loads the studio UI correctly when studio bundle is taking too long to load', () => {
it('loads the legacy studio UI correctly when studio bundle is taking too long to load', () => {
loadProjectAndRunSpec({ enableCloudStudio: false })

cy.window().then(() => {
Expand Down Expand Up @@ -149,6 +151,8 @@ describe('studio functionality', () => {
it('closes studio panel when clicking studio button (from the cloud)', () => {
launchStudio({ enableCloudStudio: true })

cy.findByTestId('studio-panel').should('be.visible')
cy.get('[data-cy="loading-studio-panel"]').should('not.exist')
cy.get('[data-cy="studio-header-studio-button"]').click()

assertClosingPanelWithoutChanges()
Expand Down Expand Up @@ -229,6 +233,9 @@ describe('studio functionality', () => {
cy.findByTestId('studio-panel')
cy.get('[data-cy="hook-name-studio commands"]')

// make sure studio is not loading
cy.get('[data-cy="loading-studio-panel"]').should('not.exist')

// Verify that AI is enabled
cy.get('[data-cy="ai-status-text"]').should('contain.text', 'Enabled')

Expand Down
37 changes: 28 additions & 9 deletions packages/app/src/runner/SpecRunnerOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
:can-access-studio-a-i="studioStore.canAccessStudioAI"
:on-studio-panel-close="handleStudioPanelClose"
:event-manager="eventManager"
:studio-status="studioStatus"
/>
</HideDuringScreenshot>
</template>
Expand All @@ -110,7 +111,7 @@
</template>

<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { REPORTER_ID, RUNNER_ID } from './utils'
import InlineSpecList from '../specs/InlineSpecList.vue'
import { getAutIframeModel, getEventManager } from '.'
Expand All @@ -125,7 +126,7 @@ import ScreenshotHelperPixels from './screenshot/ScreenshotHelperPixels.vue'
import { useScreenshotStore } from '../store/screenshot-store'
import ChooseExternalEditorModal from '@packages/frontend-shared/src/gql-components/ChooseExternalEditorModal.vue'
import { useMutation, gql } from '@urql/vue'
import { SpecRunnerOpenMode_OpenFileInIdeDocument } from '../generated/graphql'
import { SpecRunnerOpenMode_OpenFileInIdeDocument, StudioStatus_ChangeDocument } from '../generated/graphql'
import type { SpecRunnerFragment } from '../generated/graphql'
import { usePreferences } from '../composables/usePreferences'
import ScriptError from './ScriptError.vue'
Expand All @@ -140,6 +141,7 @@ import StudioInstructionsModal from './studio/StudioInstructionsModal.vue'
import StudioSaveModal from './studio/StudioSaveModal.vue'
import { useStudioStore } from '../store/studio-store'
import StudioPanel from '../studio/StudioPanel.vue'
import { useSubscription } from '../graphql'

const {
preferredMinimumPanelWidth,
Expand All @@ -166,9 +168,7 @@ fragment SpecRunner_Preferences on Query {

gql`
fragment SpecRunner_Studio on Query {
studio {
status
}
cloudStudioEnabled
}
`

Expand Down Expand Up @@ -200,6 +200,14 @@ mutation SpecRunnerOpenMode_OpenFileInIDE ($input: FileDetailsInput!) {
}
`

gql`
subscription StudioStatus_Change {
studioStatusChange {
status
}
}
`

const props = defineProps<{
gql: SpecRunnerFragment
}>()
Expand Down Expand Up @@ -243,16 +251,27 @@ const isSpecsListOpenPreferences = computed(() => {
return props.gql.localSettings.preferences.isSpecsListOpen ?? false
})

const studioStatus = computed(() => {
return props.gql.studio?.status
// Initialize with null and wait for subscription to update
const studioStatus = ref<string | null>(null)

useSubscription({ query: StudioStatus_ChangeDocument }, (_, data) => {
if (data?.studioStatusChange?.status) {
studioStatus.value = data.studioStatusChange.status
}

return data
})

const cloudStudioEnabled = computed(() => {
return props.gql.cloudStudioEnabled
})

const shouldShowStudioButton = computed(() => {
return !!props.gql.studio && studioStatus.value === 'ENABLED' && !studioStore.isOpen
return !!cloudStudioEnabled.value && !studioStore.isOpen
})

const shouldShowStudioPanel = computed(() => {
return studioStatus.value === 'ENABLED' && (studioStore.isLoading || studioStore.isActive)
return !!cloudStudioEnabled.value && (studioStore.isLoading || studioStore.isActive)
})

const hideCommandLog = runnerUiStore.hideCommandLog
Expand Down
66 changes: 54 additions & 12 deletions packages/app/src/studio/StudioPanel.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
<template>
<div v-if="error">
Error loading the panel
<div
v-if="props.studioStatus === 'INITIALIZING'"
ref="container"
>
<LoadingStudioPanel :event-manager="props.eventManager" />
</div>
<!-- these are two distinct errors: if studio status is IN_ERROR, it means that the studio bundle failed to load from the cloud -->
<!-- if there is an error in the component state, it means module federation failed to load the component -->
<div v-else-if="props.studioStatus === 'IN_ERROR'">
<div class="p-4 text-red-500 font-medium">
<div class="mb-2">
Error fetching studio bundle from cloud
</div>
</div>
</div>
<div v-else-if="error">
<div class="p-4 text-red-500 font-medium">
<div class="mb-2">
Error loading the panel
</div>
<div>{{ error }}</div>
</div>
</div>
<div
v-else
ref="container"
>
<LoadingStudioPanel :event-manager="props.eventManager" />
<LoadingStudioPanel
v-if="!ReactStudioPanel"
:event-manager="props.eventManager"
/>
</div>
</template>
<script lang="ts" setup>
Expand All @@ -27,6 +50,7 @@ const props = defineProps<{
canAccessStudioAI: boolean
onStudioPanelClose: () => void
eventManager: EventManager
studioStatus: string | null
}>()

interface StudioApp { default: StudioAppDefaultShape }
Expand All @@ -37,7 +61,11 @@ const ReactStudioPanel = ref<StudioPanelShape | null>(null)
const reactRoot = ref<Root | null>(null)

const maybeRenderReactComponent = () => {
// don't render the react component if the react studio panel has not loaded or if there is an error
// Skip rendering if studio is initializing or errored out
if (props.studioStatus === 'INITIALIZING' || props.studioStatus === 'IN_ERROR') {
return
}

if (!ReactStudioPanel.value || !!error.value) {
return
}
Expand Down Expand Up @@ -87,17 +115,31 @@ init({
onMounted(maybeRenderReactComponent)
onBeforeUnmount(unmountReactComponent)

loadRemote<StudioApp>('app-studio').then((module) => {
if (!module?.default) {
error.value = 'The panel was not loaded successfully'
watch(() => props.studioStatus, (newStatus) => {
if (newStatus === 'ENABLED') {
loadStudioComponent()
}

maybeRenderReactComponent()
}, { immediate: true })

function loadStudioComponent () {
if (ReactStudioPanel.value) {
return
}

ReactStudioPanel.value = module.default.StudioPanel
maybeRenderReactComponent()
}).catch((e) => {
error.value = e.message
})
loadRemote<StudioApp>('app-studio').then((module) => {
if (!module?.default) {
error.value = 'The panel was not loaded successfully'

return
}

ReactStudioPanel.value = module.default.StudioPanel
maybeRenderReactComponent()
}).catch((e) => {
error.value = e.message
})
}

</script>
7 changes: 7 additions & 0 deletions packages/data-context/src/actions/DataEmitterActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ abstract class DataEmitterEvents {
this._emit('specsChange')
}

/**
* Emitted when the studio manager's status changes
*/
studioStatusChange () {
this._emit('studioStatusChange')
}

/**
* Emitted when then relevant run numbers changed after querying for matching
* runs based on local commit shas
Expand Down
12 changes: 7 additions & 5 deletions packages/graphql/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,9 @@ type Query {
specPath: String!
): CloudProjectSpecResult

"""Whether cloud studio is enabled"""
cloudStudioEnabled: Boolean

"""A user within the Cypress Cloud"""
cloudViewer: CloudUser

Expand Down Expand Up @@ -2069,11 +2072,6 @@ type Query {
"""The files that have just been scaffolded"""
scaffoldedFiles: [ScaffoldedFile!]

"""
Data pertaining to studio and the studio manager that is loaded from the cloud
"""
studio: Studio

"""Previous versions of cypress and their release date"""
versions: VersionData

Expand Down Expand Up @@ -2382,6 +2380,7 @@ type Studio {
enum StudioStatusType {
ENABLED
INITIALIZED
INITIALIZING
IN_ERROR
NOT_INITIALIZED
}
Expand Down Expand Up @@ -2437,6 +2436,9 @@ type Subscription {

"""Issued when the watched specs for the project changes"""
specsChange: CurrentProject

"""Status of the studio manager"""
studioStatusChange: Studio
}

enum SupportStatusEnum {
Expand Down
20 changes: 4 additions & 16 deletions packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { Wizard } from './gql-Wizard'
import { ErrorWrapper } from './gql-ErrorWrapper'
import { CachedUser } from './gql-CachedUser'
import { Cohort } from './gql-Cohorts'
import { Studio } from './gql-Studio'
import type { StudioStatusType } from '@packages/data-context/src/gen/graphcache-config.gen'

export const Query = objectType({
name: 'Query',
Expand Down Expand Up @@ -103,20 +101,10 @@ export const Query = objectType({
resolve: (source, args, ctx) => ctx.coreData.authState,
})

t.field('studio', {
type: Studio,
description: 'Data pertaining to studio and the studio manager that is loaded from the cloud',
resolve: async (source, args, ctx) => {
const isStudioReady = ctx.coreData.studioLifecycleManager?.isStudioReady()

if (!isStudioReady) {
return { status: 'INITIALIZED' as StudioStatusType }
}

const studio = await ctx.coreData.studioLifecycleManager?.getStudio()

return studio ? { status: studio.status } : null
},
t.field('cloudStudioEnabled', {
type: 'Boolean',
description: 'Whether cloud studio is enabled',
resolve: (source, args, ctx) => ctx.coreData.studioLifecycleManager?.cloudStudioEnabled ?? false,
})

t.nonNull.field('localSettings', {
Expand Down
17 changes: 17 additions & 0 deletions packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ export const Subscription = subscriptionType({
resolve: (source, args, ctx) => ctx.lifecycleManager,
})

t.field('studioStatusChange', {
type: 'Studio',
description: 'Status of the studio manager',
subscribe: (source, args, ctx) => ctx.emitter.subscribeTo('studioStatusChange'),
resolve: async (source, args, ctx) => {
const isStudioReady = ctx.coreData.studioLifecycleManager?.isStudioReady()

if (!isStudioReady) {
return { status: 'INITIALIZING' as const }
}

const studio = await ctx.coreData.studioLifecycleManager?.getStudio()

return studio ? { status: studio.status } : null
},
})

t.field('configChange', {
type: CurrentProject,
description: 'Issued when cypress.config.js is re-executed due to a change',
Expand Down
Loading