chore: collection runner enhancements and ui clean up (#4564)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam
2024-11-28 18:25:00 +06:00
committed by GitHub
parent d2401d6ceb
commit e2e769db71
10 changed files with 215 additions and 51 deletions

View File

@@ -1215,6 +1215,7 @@
"include_active_environment": "Include active environment:", "include_active_environment": "Include active environment:",
"cli": "CLI", "cli": "CLI",
"delay": "Delay", "delay": "Delay",
"negative_delay": "Delay cannot be negative",
"ui": "Runner", "ui": "Runner",
"running_collection": "Running collection", "running_collection": "Running collection",
"run_config": "Run Configuration", "run_config": "Run Configuration",
@@ -1228,7 +1229,9 @@
"cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.", "cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.",
"cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.", "cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.",
"cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.", "cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.",
"run_collection": "Run collection" "run_collection": "Run collection",
"no_passed_tests": "No tests passed",
"no_failed_tests": "No tests failed"
}, },
"ai_experiments": { "ai_experiments": {
"generate_request_name": "Generate Request Name Using AI", "generate_request_name": "Generate Request Name Using AI",

View File

@@ -150,6 +150,7 @@ declare module 'vue' {
HttpAuthorizationNTLM: typeof import('./components/http/authorization/NTLM.vue')['default'] HttpAuthorizationNTLM: typeof import('./components/http/authorization/NTLM.vue')['default']
HttpAuthorizationOAuth2: typeof import('./components/http/authorization/OAuth2.vue')['default'] HttpAuthorizationOAuth2: typeof import('./components/http/authorization/OAuth2.vue')['default']
HttpBody: typeof import('./components/http/Body.vue')['default'] HttpBody: typeof import('./components/http/Body.vue')['default']
HttpBodyBinary: typeof import('./components/http/BodyBinary.vue')['default']
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default'] HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
HttpCodegen: typeof import('./components/http/Codegen.vue')['default'] HttpCodegen: typeof import('./components/http/Codegen.vue')['default']
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default'] HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
@@ -225,6 +226,7 @@ declare module 'vue' {
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default'] ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default'] ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default'] ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsImportSummary: typeof import('./components/importExport/ImportExportSteps/ImportSummary.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default'] ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default'] ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default'] InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default']

View File

@@ -4,7 +4,7 @@
class="w-full rounded px-4 py-3 transition cursor-pointer focus:outline-none hover:active hover:bg-primaryLight hover:text-secondaryDark" class="w-full rounded px-4 py-3 transition cursor-pointer focus:outline-none hover:active hover:bg-primaryLight hover:text-secondaryDark"
@click="selectRequest()" @click="selectRequest()"
> >
<div class="flex gap-4 mb-1"> <div class="flex gap-4 mb-1 items-center">
<span <span
class="flex items-center justify-center truncate pointer-events-none" class="flex items-center justify-center truncate pointer-events-none"
:style="{ color: requestLabelColor }" :style="{ color: requestLabelColor }"
@@ -20,7 +20,7 @@
v-if="request.response?.statusCode" v-if="request.response?.statusCode"
:class="[ :class="[
statusCategory.className, statusCategory.className,
'outlined text-xs rounded-md px-2 flex items-center', 'outlined text-[10px] rounded px-2 flex items-center',
]" ]"
> >
{{ `${request.response?.statusCode}` }} {{ `${request.response?.statusCode}` }}

View File

@@ -31,13 +31,11 @@
<HoppButtonPrimary <HoppButtonPrimary
v-if="showResult && tab.document.status === 'running'" v-if="showResult && tab.document.status === 'running'"
:label="t('test.stop')" :label="t('test.stop')"
class="w-32"
@click="stopTests()" @click="stopTests()"
/> />
<HoppButtonPrimary <HoppButtonPrimary
v-else v-else
:label="t('test.run_again')" :label="t('test.run_again')"
class="w-32"
@click="runAgain()" @click="runAgain()"
/> />
<HoppButtonSecondary <HoppButtonSecondary
@@ -115,6 +113,7 @@
collectionID: tab.document.collectionID, collectionID: tab.document.collectionID,
} }
" "
:prev-config="testRunnerConfig"
@hide-modal="showCollectionsRunnerModal = false" @hide-modal="showCollectionsRunnerModal = false"
/> />
</template> </template>
@@ -127,7 +126,8 @@ import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { pipe } from "fp-ts/lib/function" import { pipe } from "fp-ts/lib/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { computed, nextTick, onMounted, ref } from "vue" import { computed, nextTick, onMounted, ref, watch } from "vue"
import { useReadonlyStream } from "~/composables/stream"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
@@ -141,7 +141,12 @@ import {
TestRunnerCollectionsAdapter, TestRunnerCollectionsAdapter,
} from "~/helpers/runner/adapter" } from "~/helpers/runner/adapter"
import { getErrorMessage } from "~/helpers/runner/collection-tree" import { getErrorMessage } from "~/helpers/runner/collection-tree"
import { getRESTCollectionByRefId } from "~/newstore/collections" import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import {
getRESTCollectionByRefId,
getRESTCollectionInheritedProps,
restCollectionStore,
} from "~/newstore/collections"
import { HoppTab } from "~/services/tab" import { HoppTab } from "~/services/tab"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { import {
@@ -154,6 +159,12 @@ const t = useI18n()
const toast = useToast() const toast = useToast()
const colorMode = useColorMode() const colorMode = useColorMode()
const teamCollectionAdapter = new TeamCollectionAdapter(null)
const teamCollectionList = useReadonlyStream(
teamCollectionAdapter.collections$,
[]
)
const props = defineProps<{ modelValue: HoppTab<HoppTestRunnerDocument> }>() const props = defineProps<{ modelValue: HoppTab<HoppTestRunnerDocument> }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -218,8 +229,33 @@ const showResult = computed(() => {
}) })
const runTests = async () => { const runTests = async () => {
const { collectionID, collectionType } = tab.value.document
const isPersonalWorkspace = collectionType === "my-collections"
const collections = isPersonalWorkspace
? restCollectionStore.value.state
: teamCollectionList.value.map(teamCollToHoppRESTColl)
const collectionInheritedProps = getRESTCollectionInheritedProps(
collectionID,
collections,
collectionType
)
const { auth, headers } = collectionInheritedProps ?? {
auth: { authActive: true, authType: "none" },
headers: [],
}
// Accommodate collection properties for personal workspace
// TODO: Resolve the collection properties computation for team workspaces
const resolvedCollection = isPersonalWorkspace
? { ...collection.value, auth, headers }
: collection.value
testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running testRunnerStopRef.value = false // when testRunnerStopRef is false, the test runner will start running
testRunnerService.runTests(tab, collection.value, { testRunnerService.runTests(tab, resolvedCollection, {
...testRunnerConfig.value, ...testRunnerConfig.value,
stopRef: testRunnerStopRef, stopRef: testRunnerStopRef,
}) })

View File

@@ -21,6 +21,7 @@
type="number" type="number"
:label="t('collection_runner.delay')" :label="t('collection_runner.delay')"
class="!rounded-r-none !border-r-0" class="!rounded-r-none !border-r-0"
:class="{ 'border-red-500': config.delay < 0 }"
input-styles="floating-input !rounded-r-none !border-r-0" input-styles="floating-input !rounded-r-none !border-r-0"
> >
<template #button> <template #button>
@@ -31,6 +32,9 @@
</span> </span>
</template> </template>
</HoppSmartInput> </HoppSmartInput>
<p v-if="config.delay < 0" class="text-xs text-red-500 mt-1">
{{ t("collection_runner.negative_delay") }}
</p>
</div> </div>
</section> </section>
@@ -133,6 +137,7 @@
<HoppButtonPrimary <HoppButtonPrimary
v-if="activeTab === 'test-runner'" v-if="activeTab === 'test-runner'"
:label="`${t('test.run')}`" :label="`${t('test.run')}`"
:disabled="config.delay < 0"
:icon="IconPlay" :icon="IconPlay"
outline outline
@click="runTests" @click="runTests"
@@ -157,7 +162,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { computed, ref } from "vue" import { computed, onMounted, ref } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { HoppCollection } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
@@ -203,6 +208,7 @@ export type CollectionRunnerData =
const props = defineProps<{ const props = defineProps<{
sameTab?: boolean sameTab?: boolean
collectionRunnerData: CollectionRunnerData collectionRunnerData: CollectionRunnerData
prevConfig?: Partial<TestRunnerConfig>
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -230,6 +236,12 @@ const config = ref<TestRunnerConfig>({
keepVariableValues: false, keepVariableValues: false,
}) })
onMounted(() => {
if (props.prevConfig) {
config.value = { ...config.value, ...props.prevConfig }
}
})
const runTests = async () => { const runTests = async () => {
const collectionTree = await getCollectionTree( const collectionTree = await getCollectionTree(
props.collectionRunnerData.type, props.collectionRunnerData.type,

View File

@@ -27,7 +27,16 @@
</HoppSmartTabs> </HoppSmartTabs>
</div> </div>
<div class="flex flex-col justify-center test-runner pr-2"> <div
class="flex flex-col justify-center test-runner pr-2"
:class="{
hidden:
(selectedTestTab === 'passed' &&
tab.document.testRunnerMeta.passedTests === 0) ||
(selectedTestTab === 'failed' &&
tab.document.testRunnerMeta.failedTests === 0),
}"
>
<HoppSmartTree :expand-all="true" :adapter="collectionAdapter"> <HoppSmartTree :expand-all="true" :adapter="collectionAdapter">
<template #content="{ node }"> <template #content="{ node }">
<HttpTestResultFolder <HttpTestResultFolder
@@ -60,17 +69,34 @@
</template> </template>
</HoppSmartTree> </HoppSmartTree>
</div> </div>
<HoppSmartPlaceholder
v-if="
(selectedTestTab === 'passed' &&
tab.document.testRunnerMeta.passedTests === 0) ||
(selectedTestTab === 'failed' &&
tab.document.testRunnerMeta.failedTests === 0)
"
:src="`/images/states/${colorMode.value}/pack.svg`"
:text="
selectedTestTab === 'passed'
? `${t('collection_runner.no_passed_tests')}`
: `${t('collection_runner.no_failed_tests')}`
"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SmartTreeAdapter } from "@hoppscotch/ui" import { SmartTreeAdapter } from "@hoppscotch/ui"
import { ref } from "vue" import { ref } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useColorMode } from "~/composables/theming"
import { HoppTestRunnerDocument } from "~/helpers/rest/document" import { HoppTestRunnerDocument } from "~/helpers/rest/document"
import { HoppTab } from "~/services/tab" import { HoppTab } from "~/services/tab"
import { TestRunnerRequest } from "~/services/test-runner/test-runner.service" import { TestRunnerRequest } from "~/services/test-runner/test-runner.service"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode()
defineProps<{ defineProps<{
tab: HoppTab<HoppTestRunnerDocument> tab: HoppTab<HoppTestRunnerDocument>

View File

@@ -51,9 +51,12 @@ const getCollectionChildrenIDs = async (collID: string) => {
return E.left(data.left) return E.left(data.left)
} }
collsList.push(...data.right.collection!.children.map((x) => x.id)) if (!data.right.collection) return E.right([])
if (data.right.collection!.children.length !== BACKEND_PAGE_SIZE) break const children = data.right.collection.children || []
collsList.push(...children.map((x) => x.id))
if (children.length !== BACKEND_PAGE_SIZE) break
} }
return E.right(collsList) return E.right(collsList)
@@ -170,10 +173,14 @@ export const getCompleteCollectionTree = (
collectionID: collID, collectionID: collID,
}, },
}), }),
TE.map((result) => ({ TE.map((result) =>
result.collection
? {
title: result.collection!.title, title: result.collection!.title,
data: result.collection!.data, data: result.collection!.data,
})) }
: null
)
) )
), ),
TE.bind("children", () => TE.bind("children", () =>
@@ -192,8 +199,8 @@ export const getCompleteCollectionTree = (
id: collID, id: collID,
children, children,
requests, requests,
title: titleAndData.title, title: titleAndData?.title,
data: titleAndData.data, data: titleAndData?.data,
} }
) )
) )

View File

@@ -1217,7 +1217,72 @@ export function getRESTCollection(collectionIndex: number) {
return restCollectionStore.value.state[collectionIndex] return restCollectionStore.value.state[collectionIndex]
} }
export function getRESTCollectionByRefId(ref_id: string) { function computeCollectionInheritedProps(
collection: HoppCollection,
ref_id: string,
type: "my-collections" | "team-collections" = "my-collections",
parentAuth: HoppRESTAuth | null = null,
parentHeaders: HoppRESTHeaders | null = null
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
// Determine the inherited authentication and headers
const inheritedAuth =
collection.auth?.authType === "inherit" && collection.auth.authActive
? (parentAuth ?? { authType: "none", authActive: false })
: (collection.auth ?? { authType: "none", authActive: false })
const inheritedHeaders: HoppRESTHeaders = [
...(parentHeaders ?? []),
...collection.headers,
]
// Check if the current collection matches the target reference ID
const isTargetCollection =
type === "my-collections"
? collection._ref_id === ref_id
: collection.id === ref_id
if (isTargetCollection) {
return {
auth: inheritedAuth,
headers: inheritedHeaders,
}
}
// Recursively search in folders
for (const folder of collection.folders) {
const result = computeCollectionInheritedProps(
folder,
ref_id,
type,
inheritedAuth,
inheritedHeaders
)
if (result) return result // Return as soon as a match is found
}
return null
}
export function getRESTCollectionInheritedProps(
collectionID: string,
collections: HoppCollection[] = restCollectionStore.value.state,
type: "my-collections" | "team-collections" = "my-collections"
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
for (const collection of collections) {
const result = computeCollectionInheritedProps(
collection,
collectionID,
type
)
if (result) {
return result
}
}
return null
}
function findCollection( function findCollection(
collection: HoppCollection, collection: HoppCollection,
ref_id: string ref_id: string
@@ -1233,6 +1298,8 @@ export function getRESTCollectionByRefId(ref_id: string) {
} }
return null return null
} }
export function getRESTCollectionByRefId(ref_id: string) {
for (const collection of restCollectionStore.value.state) { for (const collection of restCollectionStore.value.state) {
const found = findCollection(collection, ref_id) const found = findCollection(collection, ref_id)
if (found) { if (found) {

View File

@@ -510,25 +510,6 @@ export const REST_TAB_STATE_SCHEMA = z
z.object({ z.object({
tabID: z.string(), tabID: z.string(),
doc: z.union([ doc: z.union([
z.object({
// !Versioned entity
request: entityReference(HoppRESTRequest),
type: z.literal("request").catch("request"),
isDirty: z.boolean(),
saveContext: z.optional(HoppRESTSaveContextSchema),
response: z.optional(z.nullable(HoppRESTResponseSchema)),
testResults: z.optional(z.nullable(HoppTestResultSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validRestOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
cancelFunction: z.optional(z.function()),
}),
z.object({
type: z.literal("example-response").catch("example-response"),
response: HoppRESTRequestResponse,
saveContext: z.optional(HoppRESTSaveContextSchema),
isDirty: z.boolean(),
}),
z.object({ z.object({
type: z.literal("test-runner").catch("test-runner"), type: z.literal("test-runner").catch("test-runner"),
config: z.object({ config: z.object({
@@ -556,6 +537,25 @@ export const REST_TAB_STATE_SCHEMA = z
testResults: z.optional(z.nullable(HoppTestResultSchema)), testResults: z.optional(z.nullable(HoppTestResultSchema)),
isDirty: z.boolean(), isDirty: z.boolean(),
}), }),
z.object({
// !Versioned entity
request: entityReference(HoppRESTRequest),
type: z.literal("request").catch("request"),
isDirty: z.boolean(),
saveContext: z.optional(HoppRESTSaveContextSchema),
response: z.optional(z.nullable(HoppRESTResponseSchema)),
testResults: z.optional(z.nullable(HoppTestResultSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validRestOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
cancelFunction: z.optional(z.function()),
}),
z.object({
type: z.literal("example-response").catch("example-response"),
response: HoppRESTRequestResponse,
saveContext: z.optional(HoppRESTSaveContextSchema),
isDirty: z.boolean(),
}),
]), ]),
}) })
), ),

View File

@@ -39,6 +39,17 @@ export class RESTTabService extends TabService<HoppTabDocument> {
} }
} }
if (tab.document.type === "test-runner") {
return {
tabID: tab.id,
doc: {
...tab.document,
request: null,
response: null,
},
}
}
return { return {
tabID: tab.id, tabID: tab.id,
doc: { doc: {