feat: create the front end for runs in the launchpad (#17692)

* make sidebar item screen size smaller to zoom

* create progressCircle element

* fix configFile test

* front end for runs

* add useful helper for mounting fragment

* types

* update mount function

* apply review comments

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
This commit is contained in:
Barthélémy Ledoux
2021-08-10 23:11:51 -05:00
committed by GitHub
parent 7a59de1202
commit da827512df
16 changed files with 363 additions and 31 deletions
+66 -6
View File
@@ -84,11 +84,71 @@ function mountFragment<Result, Variables, T extends TypedDocumentNode<Result, Va
}).then(() => ctx)
}
function mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>> (source: T[], options: MountFragmentConfig<T>): Cypress.Chainable<ClientTestContext> {
const ctx = new ClientTestContext()
return mount(defineComponent({
name: `mountFragmentList`,
setup () {
const getTypeCondition = (source: any) => (source.definitions[0] as any).typeCondition.name.value.toLowerCase()
const frags = source.map((src) => {
/**
* generates something like
* wizard {
* ... MyFragment
* }
*
* for each fragment passed in.
*/
const parent = getTypeCondition(src)
return `${parent} {
...${(src.definitions[0] as FragmentDefinitionNode).name.value}
}`
})
const query = `
query MountFragmentTest {
${frags.join('\n')}
}
${source.map(print)}
`
const result = useQuery({
query,
})
return {
gql: computed(() => result.data.value),
}
},
render: (props) => {
return props.gql ? options.render(props.gql) : h('div')
},
}), {
global: {
stubs: {
transition: false,
},
plugins: [
createI18n(),
{
install (app) {
app.use(urql, testApolloClient({
context: ctx,
rootValue: options.type(ctx),
}))
},
},
],
},
}).then(() => ctx)
}
Cypress.Commands.add('mountFragment', mountFragment)
Cypress.Commands.add('mountFragmentList', (source, options) => {
return mountFragment(source, options, true)
})
Cypress.Commands.add('mountFragmentList', mountFragmentList)
type GetRootType<T> = T extends TypedDocumentNode<infer U, any>
? U extends { __typename?: infer V }
@@ -106,7 +166,7 @@ type MountFragmentConfig<T extends TypedDocumentNode> = {
type MountFragmentListConfig<T extends TypedDocumentNode> = {
variables?: T['__variablesType']
render: (frag: Exclude<T['__resultType'], undefined>[]) => JSX.Element
render: (frag: Exclude<T['__resultType'], undefined>) => JSX.Element
type: (ctx: ClientTestContext) => GetRootType<T>[]
} & CyMountOptions<unknown>
@@ -127,8 +187,8 @@ declare global {
/**
* Mount helper for a component with a GraphQL fragment, as a list
*/
mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>>(
fragment: T,
mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>>(
fragment: T[],
config: MountFragmentListConfig<T>
): Cypress.Chainable<ClientTestContext>
}
@@ -0,0 +1,9 @@
import ProgressCircle from './ProgressCircle.vue'
describe('<ProgressCircle />', { viewportWidth: 50, viewportHeight: 50 }, () => {
it('playground', () => {
cy.mount(() => (
<ProgressCircle progress={57} radius={25} stroke={3} class="text-indigo-400" />
))
})
})
@@ -0,0 +1,51 @@
<template>
<svg
:height="radius * 2"
:width="radius * 2"
>
<circle
stroke="#EBEBEB"
fill="transparent"
:stroke-width="stroke"
:r="normalizedRadius"
:cx="radius"
:cy="radius"
/>
<circle
stroke="currentColor"
fill="transparent"
:stroke-dasharray="circumference + ' ' + circumference"
:style="{ strokeDashoffset }"
:stroke-width="stroke"
:r="normalizedRadius"
:cx="radius"
:cy="radius"
stroke-linecap="round"
/>
</svg>
</template>
<script lang="ts" setup>
import { computed } from "vue"
const props = defineProps<{
radius: number
stroke: number
progress: number
}>()
const normalizedRadius = props.radius - props.stroke * 2;
const circumference = normalizedRadius * 2 * Math.PI;
const strokeDashoffset = computed(() => {
return circumference - props.progress / 100 * circumference;
})
</script>
<style scoped>
svg{
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
</style>
@@ -2,7 +2,7 @@ import { ref } from 'vue'
import SideBarItem from './SideBarItem.vue'
import IconCoffee from 'virtual:vite-icons/mdi/coffee'
describe('<SideBarItem />', () => {
describe('<SideBarItem />', { viewportWidth: 200, viewportHeight: 150 }, () => {
it('playground', () => {
const active = ref(0)
const onClick = (value) => {
@@ -0,0 +1,41 @@
import RunCard from './RunCard.vue'
describe('<RunCard />', { viewportHeight: 400 }, () => {
it('playground', () => {
cy.mount(() => (
<div class="bg-gray-100 h-screen p-3">
<RunCard
status="ko"
name="Updating the hover state for the button component"
branch="master"
author="Ryan"
timestamp={new Date().getTime()}
results={{ pass: 5, fail: 0, skip: 0, flake: 2 }}/>
<RunCard
status="warn"
name="Fixing broken tests"
branch="master"
author="Ryan"
timestamp={new Date().getTime()}
results={{ pass: 15, fail: 1, skip: 0, flake: 3 }}
/>
<RunCard
status="ok"
name="Adding a hover state to the button component"
branch="master"
author="Ryan"
timestamp={new Date().getTime()}
results={{ pass: 20, fail: 2, skip: 0, flake: 0 }}
/>
<RunCard
status={25}
name="In progress"
branch="master"
author="Bart"
timestamp={new Date().getTime()}
results={{ pass: 12, fail: 0, skip: 0, flake: 0 }}
/>
</div>
))
})
})
+54
View File
@@ -0,0 +1,54 @@
<template>
<div class="h-18 border border-gray-200 rounded bg-white flex items-center mb-2 box-border">
<div class="w-18 flex items-center justify-center">
<RunIcon :status="props.status" />
</div>
<div class="pl-4 border-l border-gray-200 flex-grow">
<h2 class="font-medium text-indigo-500 leading-4">{{ props.name }}</h2>
<div class="flex">
<span v-for="info in runInfo" class="flex items-center mr-3 mt-1">
<component v-if="info.icon" :is="info.icon" class="mr-1 text-gray-500 text-sm" />
<span class="text-gray-500 text-sm font-light">
{{info.text}}
</span>
</span>
</div>
</div>
<RunResults v-bind="props.results" class="m-6 ml-0" />
</div>
</template>
<script lang="ts" setup>
import RunIcon from './RunIcon.vue'
import RunResults from './RunResults.vue'
// bx:bx-user-circle
import IconUserCircle from 'virtual:vite-icons/bx/bx-user-circle'
// carbon:branch
import IconBranch from 'virtual:vite-icons/carbon/branch'
const props = defineProps<{
name: string
status: "ok" | "ko" | "warn" | number
author: string
branch: string
timestamp: number
results: {
flake: number
skip: number
pass: number
fail: number
}
}>()
const runInfo = [{
text: props.author,
icon: IconUserCircle
},
{
text: props.branch,
icon: IconBranch
},
{
text: new Date(props.timestamp).toLocaleTimeString()
}]
</script>
@@ -0,0 +1,17 @@
import RunIcon from './RunIcon.vue'
describe('<RunIcon />', { viewportWidth: 80, viewportHeight: 200 }, () => {
it('playground', () => {
cy.mount(() => (
<div class="p-3 flex flex-col align-middle justify-center w-screen">
<RunIcon status="ko"/>
<hr/>
<RunIcon status="ok"/>
<hr/>
<RunIcon status="warn"/>
<hr/>
<RunIcon status={45} />
</div>
))
})
})
+23
View File
@@ -0,0 +1,23 @@
<template>
<IconPass v-if="props.status === 'ok'" class="text-green-500 text-xl" />
<IconFail v-else-if="props.status === 'ko'" class="text-red-500 text-xl"/>
<IconWarn v-else-if="props.status === 'warn'" class="text-orange-400 text-xl"/>
<ProgressCircle v-else :progress="progress" :radius="12" :stroke="2" class="text-indigo-400"/>
</template>
<script lang="ts" setup>
// eva:checkmark-circle-2-fill
import IconPass from 'virtual:vite-icons/eva/checkmark-circle-2-fill'
// eva:close-circle-fill
import IconFail from 'virtual:vite-icons/eva/close-circle-fill'
// dashicons:warning
import IconWarn from 'virtual:vite-icons/dashicons/warning'
import ProgressCircle from "../components/progress/ProgressCircle.vue"
const props = defineProps<{
status: "ok" | "ko" | "warn" | number
}>()
const progress = typeof props.status === 'number' ? props.status : 0
</script>
@@ -0,0 +1,9 @@
import RunResults from './RunResults.vue'
const results = { pass: 5, fail: 0, skip: 0, flake: 2 }
describe('<RunResults />', () => {
it('playground', () => {
cy.mount(() => (<RunResults {...results}/>))
})
})
@@ -0,0 +1,39 @@
<template>
<div class="h-7 border border-gray-200 rounded flex text-gray-500" :class="class">
<div class="flex items-center p-2">
<IconFlake class="text-gray-400 text-sm mr-1" />
{{props.flake}}
</div>
<div class="flex items-center p-2">
<IconCancel class="text-gray-400 text-sm mr-1" />
{{props.skip}}
</div>
<div class="flex items-center p-2">
<IconPass class="text-green-600 text-xs mr-1" />
{{props.pass}}
</div>
<div class="flex items-center p-2">
<IconFail class="text-red-600 mr-1" />
{{props.fail}}
</div>
</div>
</template>
<script lang="ts" setup>
// bi:check-lg
import IconPass from 'virtual:vite-icons/bi/check-lg'
// eva:close-fill
import IconFail from 'virtual:vite-icons/eva/close-fill'
// fa-solid:snowflake
import IconFlake from 'virtual:vite-icons/fa-solid/snowflake'
// line-md:cancel
import IconCancel from 'virtual:vite-icons/line-md/cancel'
const props = defineProps<{
flake: number
skip: number
pass: number
fail: number
class?: string
}>()
</script>
@@ -0,0 +1,9 @@
import RunsPage from './RunsPage.vue'
describe('<RunsPage />', () => {
it('playground', () => {
cy.mount(() => (
<RunsPage />
))
})
})
+13
View File
@@ -0,0 +1,13 @@
<template>
<main class="min-w-650px max-w-800px">
<RunCard v-for="run in runs" v-bind="run"/>
</main>
</template>
<script lang="ts" setup>
import RunCard from "./RunCard.vue";
// wire gql here
const runs = []
</script>
@@ -1,32 +1,37 @@
import { ref } from 'vue'
import { ConfigFileFragmentDoc } from '../generated/graphql'
import {
ConfigFileFragment,
ConfigFileFragmentDoc,
ProjectRootFragment,
ProjectRootFragmentDoc,
} from '../generated/graphql'
import ConfigFile from './ConfigFile.vue'
describe('<ConfigFile />', () => {
beforeEach(() => {
const display = ref(false)
cy.mountFragment(ConfigFileFragmentDoc, {
cy.mountFragmentList([
ConfigFileFragmentDoc,
ProjectRootFragmentDoc,
], {
type: (ctx) => {
ctx.wizard.setFramework('nuxtjs')
ctx.wizard.setFramework('cra')
ctx.wizard.setBundler('webpack')
return ctx.wizard
return [ctx.wizard, ctx.app]
},
render: (gqlVal) => (
<div class="m-10">
<button
data-cy="show"
onClick={() => {
display.value = true
}}
class="hidden"
></button>
{display.value ? <ConfigFile gql={gqlVal} /> : undefined}
</div>
),
})
render: (gql) => {
// @ts-ignore - TODO: fix types
const wizard = gql.wizard as any as ConfigFileFragment
// @ts-ignore - TODO: fix types
const app = gql.app as any as ProjectRootFragment
cy.get('[data-cy="show"]').click({ force: true })
return (
<ConfigFile
wizard={wizard}
app={app}
/>
)
},
})
})
it('playground', { viewportWidth: 1280, viewportHeight: 1024 }, () => {
+1 -1
View File
@@ -85,7 +85,7 @@ mutation appCreateConfigFile($code: String!, $configFilename: String!) {
const props = defineProps<{
wizard: ConfigFileFragment
app: ProjectRootFragment | undefined
app: ProjectRootFragment
}>()
@@ -10,7 +10,9 @@ describe('<InstallDependencies />', () => {
return ctx.wizard
},
render: (gqlVal) => <InstallDependencies gql={gqlVal} />,
render: (gqlVal) => {
return <InstallDependencies gql={gqlVal} />
},
})
})
+1 -1
View File
@@ -69,7 +69,7 @@ export default defineComponent({
return {
loading: result.fetching,
wizard: computed(() => result.data.value?.wizard),
app: computed(() => result.data.value?.app)
app: computed(() => result.data.value?.app!)
};
},
});