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

Merged
merged 27 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4cf7e8a
internal: show loading and error statuses in studio panel, add downlo…
astone123 May 2, 2025
0301ca9
Merge branch 'develop' into studio-error-handling
astone123 May 5, 2025
9db8b07
update lifecycle manager tests
astone123 May 5, 2025
1859ba2
fix build
astone123 May 5, 2025
090cf08
fix build for real
astone123 May 5, 2025
efd5dfb
fix logic
astone123 May 6, 2025
d77f778
fix types
astone123 May 6, 2025
1b8cb7d
Merge branch 'develop' into studio-error-handling
astone123 May 6, 2025
9c1d064
Merge branch 'develop' into studio-error-handling
astone123 May 6, 2025
12b58f2
types, tests, etc
astone123 May 6, 2025
29714f2
Merge branch 'develop' into studio-error-handling
astone123 May 6, 2025
46518ea
Update packages/app/src/studio/StudioPanel.vue
astone123 May 8, 2025
4376aa5
Merge branch 'develop' into studio-error-handling
astone123 May 8, 2025
cd100a0
use getter
astone123 May 8, 2025
6d33c5f
Merge branch 'develop' into studio-error-handling
astone123 May 12, 2025
3a7bc96
fix types
astone123 May 12, 2025
9a3c436
some feedback
astone123 May 12, 2025
fed29ce
some feedback
astone123 May 12, 2025
26bf729
Merge branch 'develop' into studio-error-handling
astone123 May 12, 2025
82ed973
re-name cloudStudioEnabled
astone123 May 12, 2025
7bb3f97
fix status updates
astone123 May 12, 2025
4cd6313
feedback
astone123 May 12, 2025
6f386a2
fix types
astone123 May 12, 2025
45f36d7
fix tests
astone123 May 13, 2025
317f1e9
fix canAccessStudioAI status
astone123 May 13, 2025
d2f53d8
Merge branch 'develop' into studio-error-handling
astone123 May 13, 2025
0ed2035
fix test
astone123 May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading