Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"devDependencies": {
"@cypress-design/icon-registry": "^1.5.1",
"@cypress-design/vue-button": "^1.6.0",
"@cypress-design/vue-icon": "^1.18.0",
"@cypress-design/vue-icon": "^1.33.0",
"@cypress-design/vue-spinner": "^1.0.0",
"@cypress-design/vue-statusicon": "^1.0.0",
"@cypress-design/vue-tabs": "^1.2.2",
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/runner/SpecRunnerOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
:on-studio-panel-close="handleStudioPanelClose"
:event-manager="eventManager"
:studio-status="studioStatus"
:is-cert-error="isCertError"
:aut-url-selector="autUrlSelector"
:user-project-status-store="userProjectStatusStore"
:has-requested-project-access="hasRequestedProjectAccess"
Expand Down Expand Up @@ -248,6 +249,7 @@ gql`
subscription StudioStatus_Change {
studioStatusChange {
status
isCertError
canAccessStudioAI
}
}
Expand Down Expand Up @@ -304,12 +306,14 @@ const isSpecsListOpenPreferences = computed(() => {
return props.gql.localSettings.preferences.isSpecsListOpen ?? false
})

// Initialize with null and wait for subscription to update
// Initialize and wait for subscription to update
const studioStatus = ref<string | null>(null)
const isCertError = ref<boolean | null>(null)

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

Expand Down
47 changes: 34 additions & 13 deletions packages/app/src/studio/StudioErrorPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
container-class="text-center"
>
<div class="relative">
<IconTechnologyDashboardFail
size="48"
stroke-color="gray-500"
fill-color="gray-900"
secondary-fill-color="red-200"
secondary-stroke-color="red-500"
/>
<component :is="props.icon" />
</div>

<div class="flex flex-col items-center gap-[4px] max-w-[448px]">
<h2 class="text-white text-[16px] leading-[24px] font-medium">
Something went wrong
{{ props.title }}
</h2>
<p class="text-gray-400 text-[16px] leading-[24px]">
There was a problem with Cypress Studio. Our team has been notified.
If the problem persists, please try again later.
{{ props.message }}
</p>
</div>

<Button
v-if="props.showLearnMore"
variant="outline-dark"
size="32"
data-cy="studio-error-learn-more-button"
@click="() => props.eventManager?.ws?.emit('external:open', props.learnMoreUrl)"
>
Learn more
</Button>
<Button
variant="outline-dark"
size="32"
data-cy="studio-error-retry-button"
@click="onRetry"
@click="props.onRetry"
>
<IconActionRefresh
size="16"
Expand All @@ -41,13 +43,32 @@
</template>

<script lang="ts" setup>
import { withDefaults, h } from 'vue'
import Button from '@cypress-design/vue-button'
import { IconTechnologyDashboardFail, IconActionRefresh } from '@cypress-design/vue-icon'
import StudioPanelContainer from './StudioPanelContainer.vue'
import type { EventManager } from '../runner/event-manager'

const props = defineProps<{
const props = withDefaults(defineProps<{
eventManager: EventManager
title?: string
message?: string
icon?: any
showLearnMore?: boolean
learnMoreUrl?: string
onRetry: () => void
}>()
}>(), {
title: 'Something went wrong',
message: 'There was a problem with Cypress Studio. Our team has been notified. If the problem persists, please try again later.',
icon: () => {
return h(IconTechnologyDashboardFail, {
size: '48',
'stroke-color': 'gray-500',
'fill-color': 'gray-900',
'secondary-fill-color': 'red-200',
'secondary-stroke-color': 'red-500',
})
},
learnMoreUrl: 'https://on.cypress.io/proxy-configuration',
})
</script>
33 changes: 31 additions & 2 deletions packages/app/src/studio/StudioPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
<div v-else-if="props.studioStatus === 'IN_ERROR' || error">
<StudioErrorPanel
:event-manager="props.eventManager"
:on-retry="handleRetry"
:title="errorPanelProps.title"
:message="errorPanelProps.message"
:icon="errorPanelProps.icon"
:show-learn-more="errorPanelProps.showLearnMore"
:learn-more-url="errorPanelProps.learnMoreUrl"
:on-retry="errorPanelProps.onRetry"
/>
</div>
<div
Expand All @@ -25,14 +30,15 @@
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { ref, onMounted, onBeforeUnmount, watch, computed, h } from 'vue'
import { init, loadRemote, registerRemotes } from '@module-federation/runtime'
import type { StudioAppDefaultShape, StudioPanelShape } from './studio-app-types'
import type { UserProjectStatusStore } from '@cy/store/user-project-status-store'
import LoadingStudioPanel from './LoadingStudioPanel.vue'
import StudioErrorPanel from './StudioErrorPanel.vue'
import type { EventManager } from '../runner/event-manager'
import { useMutation, gql, UseMutationResponse } from '@urql/vue'
import { IconCypressStudio } from '@cypress-design/vue-icon'

// Mirrors the ReactDOM.Root type since incorporating those types
// messes up vue typing elsewhere
Expand All @@ -52,6 +58,7 @@ const props = defineProps<{
onStudioPanelClose: () => void
eventManager: EventManager
studioStatus: string | null
isCertError?: boolean | null
cloudStudioSessionId?: string
autUrlSelector: string
userProjectStatusStore: UserProjectStatusStore
Expand All @@ -68,6 +75,28 @@ const containerReactRootMap = new WeakMap<HTMLElement, Root>()

const retryStudioMutation = useMutation(retryStudioMutationGql)

const errorPanelProps = computed(() => {
if (props.isCertError) {
return {
title: 'Configure your proxy to use Cypress Studio',
message: 'Cypress Studio requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.',
icon: () => {
return h(IconCypressStudio, {
size: '48',
'fill-color': 'gray-700',
})
},
showLearnMore: true,
learnMoreUrl: 'https://on.cypress.io/proxy-configuration',
onRetry: handleRetry,
}
}

return {
onRetry: handleRetry,
}
})

const maybeRenderReactComponent = () => {
// Skip rendering if studio is initializing or errored out
if (props.studioStatus === 'INITIALIZING' || props.studioStatus === 'IN_ERROR') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const StudioStatusPayload = objectType({
type: StudioStatusTypeEnum,
})

t.boolean('isCertError', {
description: 'Whether the studio status is an IN_ERROR due to a certificate error',
})

t.nonNull.boolean('canAccessStudioAI')
},
})
Expand Down Expand Up @@ -70,6 +74,7 @@ export const Subscription = subscriptionType({
if (currentStatus === 'IN_ERROR') {
return {
status: 'IN_ERROR' as const,
isCertError: ctx.coreData.studioLifecycleManager?.getIsCertError(),
canAccessStudioAI: false,
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/data-context/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,9 @@ enum SpecType {

type StudioStatusPayload {
canAccessStudioAI: Boolean!

"""Whether the studio status is an IN_ERROR due to a certificate error"""
isCertError: Boolean
status: StudioStatusType!
}

Expand Down
2 changes: 1 addition & 1 deletion packages/driver/src/cypress/error_messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1356,7 +1356,7 @@ export default {
},
promptProxyError: {
message: stripIndent`\
\`cy.prompt\` requires an internet connection to work. To continue, you may need to configure Cypress with your proxy settings.
\`cy.prompt\` requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.
`,
docsUrl: 'https://on.cypress.io/proxy-configuration',
},
Expand Down
9 changes: 9 additions & 0 deletions packages/server/lib/cloud/api/studio/get_studio_bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { createWriteStream } from 'fs'
import { verifySignatureFromFile } from '../../encryption'
import { HttpError } from '../../network/http_error'
import { SystemError } from '../../network/system_error'
import Debug from 'debug'

const pkg = require('@packages/root')
const _delay = linearDelay(500)
const debug = Debug('cypress:server:cloud:api:studio:get_studio_bundle')
const DEFAULT_TIMEOUT = 25000

export const getStudioBundle = async ({ studioUrl, bundlePath }: { studioUrl: string, bundlePath: string }): Promise<string> => {
Expand All @@ -24,6 +26,8 @@ export const getStudioBundle = async ({ studioUrl, bundlePath }: { studioUrl: st
}, DEFAULT_TIMEOUT)

try {
debug('Fetching studio bundle from %s', studioUrl)

const response = await fetch(studioUrl, {
// @ts-expect-error - this is supported
agent: strictAgent,
Expand Down Expand Up @@ -70,7 +74,10 @@ export const getStudioBundle = async ({ studioUrl, bundlePath }: { studioUrl: st

clearTimeout(fetchTimeout)
} catch (error) {
debug('Error fetching studio bundle from %s: %o', studioUrl, error)

clearTimeout(fetchTimeout)

if (error.name === 'AbortError') {
throw new Error('Studio bundle fetch timed out')
}
Expand Down Expand Up @@ -108,5 +115,7 @@ export const getStudioBundle = async ({ studioUrl, bundlePath }: { studioUrl: st
throw new Error('Unable to verify studio signature')
}

debug('Studio bundle fetched successfully from %s', studioUrl)

return responseManifestSignature
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const NON_RETRIABLE_CERT_ERROR_CODES = Object.freeze({
SELF_SIGNED_CERT_IN_CHAIN: 'SELF_SIGNED_CERT_IN_CHAIN',
// The issuer certificate is not available locally
UNABLE_TO_GET_ISSUER_CERT_LOCALLY: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
// The certificate is expired
CERT_HAS_EXPIRED: 'CERT_HAS_EXPIRED',
})

type NonRetriableCertErrorCode = typeof NON_RETRIABLE_CERT_ERROR_CODES[keyof typeof NON_RETRIABLE_CERT_ERROR_CODES]
Expand Down
Loading
Loading