feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "Go forward",
|
||||||
"group_by": "Group by",
|
"group_by": "Group by",
|
||||||
|
"hide_secret": "Hide secret",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
"less": "Less",
|
"less": "Less",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
|
"show_secret": "Show secret",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -238,6 +240,7 @@
|
|||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
|
"secret_environments": "Secret environments are not synced to Hoppscotch hence user has to provide it's value during run-time.",
|
||||||
"shared_requests": "Shared requests are empty",
|
"shared_requests": "Shared requests are empty",
|
||||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
@@ -269,6 +272,8 @@
|
|||||||
"quick_peek": "Environment Quick Peek",
|
"quick_peek": "Environment Quick Peek",
|
||||||
"replace_with_variable": "Replace with variable",
|
"replace_with_variable": "Replace with variable",
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
|
"secret": "Secret",
|
||||||
|
"secret_value": "Secret value",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
"set": "Set environment",
|
"set": "Set environment",
|
||||||
"set_as_environment": "Set as environment",
|
"set_as_environment": "Set as environment",
|
||||||
@@ -277,6 +282,7 @@
|
|||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
|
"variables":"Variables",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -413,6 +419,8 @@
|
|||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
"environment": {
|
"environment": {
|
||||||
"add_environment": "Add to Environment",
|
"add_environment": "Add to Environment",
|
||||||
|
"add_environment_value": "Add value",
|
||||||
|
"empty_value": "Environment value is empty for the variable '{variable}' ",
|
||||||
"not_found": "Environment variable “{environment}” not found."
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<SmartEnvInput
|
||||||
v-model="editingValue"
|
v-model="editingValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="input"
|
class="input"
|
||||||
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
|
|||||||
addGlobalEnvVariable({
|
addGlobalEnvVariable({
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else if (scope.value.type === "my-environment") {
|
} else if (scope.value.type === "my-environment") {
|
||||||
addEnvironmentVariable(scope.value.index, {
|
addEnvironmentVariable(scope.value.index, {
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: editingValue.value,
|
value: editingValue.value,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
|
||||||
@@ -340,13 +340,13 @@ const showImportFailedError = () => {
|
|||||||
|
|
||||||
const handleImportToStore = async (
|
const handleImportToStore = async (
|
||||||
environments: Environment[],
|
environments: Environment[],
|
||||||
globalEnv?: Environment
|
globalEnv?: NonSecretEnvironment
|
||||||
) => {
|
) => {
|
||||||
// if there's a global env, add them to the store
|
// if there's a global env, add them to the store
|
||||||
if (globalEnv) {
|
if (globalEnv) {
|
||||||
globalEnv.variables.forEach(({ key, value }) => {
|
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||||
addGlobalEnvVariable({ key, value })
|
addGlobalEnvVariable({ key, value, secret })
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
|
|||||||
@@ -210,7 +210,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||||
@@ -265,7 +268,10 @@
|
|||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
<template v-if="variable.secret"> ******** </template>
|
||||||
|
<template v-else>
|
||||||
|
{{ variable.value }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
|
|||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: props.modelValue.index,
|
index: props.modelValue.index,
|
||||||
name: props.modelValue.environment?.name,
|
name: props.modelValue.environment?.name,
|
||||||
|
variables: props.modelValue.environment?.variables,
|
||||||
}
|
}
|
||||||
} else if (props.modelValue?.type === "team-environment") {
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
return {
|
return {
|
||||||
type: "TEAM_ENV",
|
type: "TEAM_ENV",
|
||||||
name: props.modelValue.environment.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: props.modelValue.environment.id,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
|
variables: props.modelValue.environment.environment.variables,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: "global", name: "Global" }
|
return {
|
||||||
|
type: "global",
|
||||||
|
name: "Global",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
const environment =
|
const environment =
|
||||||
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const editGlobalEnv = () => {
|
const editGlobalEnv = () => {
|
||||||
invokeAction("modals.my.environment.edit", {
|
invokeAction("modals.global.environment.update", {})
|
||||||
envName: "Global",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editEnv = () => {
|
const editEnv = () => {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:env-vars="envVars"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsAdd
|
<EnvironmentsAdd
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="showConfirmRemoveEnvModal"
|
:show="showConfirmRemoveEnvModal"
|
||||||
:title="t('confirm.remove_team')"
|
:title="`${t('confirm.remove_environment')}`"
|
||||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||||
@resolve="removeSelectedEnvironment()"
|
@resolve="removeSelectedEnvironment()"
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
|
|||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
|
|||||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
const globalEnvironment = computed(() => ({
|
const globalEnvironment = computed(() => ({
|
||||||
|
v: 1 as const,
|
||||||
|
id: "Global",
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: globalEnv.value,
|
variables: globalEnv.value,
|
||||||
}))
|
}))
|
||||||
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
|
|||||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
const editingVariableValue = ref("")
|
const editingVariableValue = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const position = ref({ top: 0, left: 0 })
|
const position = ref({ top: 0, left: 0 })
|
||||||
|
|
||||||
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
|||||||
const editEnvironment = (environmentIndex: "Global") => {
|
const editEnvironment = (environmentIndex: "Global") => {
|
||||||
editingEnvironmentIndex.value = environmentIndex
|
editingEnvironmentIndex.value = environmentIndex
|
||||||
action.value = "edit"
|
action.value = "edit"
|
||||||
|
editingVariableName.value = ""
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
|
|||||||
|
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
editingVariableValue.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("modals.environment.new", () => {
|
defineActionHandler("modals.environment.new", () => {
|
||||||
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
|||||||
showConfirmRemoveEnvModal.value = true
|
showConfirmRemoveEnvModal.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const additionalVars = ref<Environment["variables"]>([])
|
||||||
|
|
||||||
|
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.global.environment.update",
|
||||||
({ envName, variableName }) => {
|
({ variables, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variables) {
|
||||||
envName === "Global" && editEnvironment("Global")
|
additionalVars.value = variables
|
||||||
|
}
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
editEnvironment("Global")
|
||||||
|
editingVariableName.value = "Global"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,76 +16,103 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
/>
|
|
||||||
<div class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconHelpCircle"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="clearIcon"
|
||||||
|
@click="clearContent()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import { clone } from "lodash-es"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { environmentsStore } from "~/newstore/environments"
|
import { environmentsStore } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
import { uniqueId } from "lodash-es"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
|
||||||
value: string
|
value: string
|
||||||
|
key: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +186,7 @@ const props = withDefaults(
|
|||||||
action: "edit" | "new"
|
action: "edit" | "new"
|
||||||
editingEnvironmentIndex?: number | "Global" | null
|
editingEnvironmentIndex?: number | "Global" | null
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -162,6 +194,7 @@ const props = withDefaults(
|
|||||||
action: "edit",
|
action: "edit",
|
||||||
editingEnvironmentIndex: null,
|
editingEnvironmentIndex: null,
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -172,11 +205,55 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secret"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string>("")
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
|||||||
|
|
||||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const workingEnv = computed(() => {
|
const workingEnv = computed(() => {
|
||||||
if (props.editingEnvironmentIndex === "Global") {
|
if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
const vars =
|
||||||
|
props.editingVariableName === "Global"
|
||||||
|
? props.envVars()
|
||||||
|
: getGlobalVariables()
|
||||||
return {
|
return {
|
||||||
name: "Global",
|
name: "Global",
|
||||||
variables: getGlobalVariables(),
|
variables: vars,
|
||||||
} as Environment
|
} as Environment
|
||||||
} else if (props.action === "new") {
|
} else if (props.action === "new") {
|
||||||
return {
|
return {
|
||||||
|
id: uniqueId(),
|
||||||
name: "",
|
name: "",
|
||||||
variables: props.envVars(),
|
variables: props.envVars(),
|
||||||
}
|
}
|
||||||
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
|
|||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
variables,
|
variables,
|
||||||
|
A.filter(({ secret }) => !secret),
|
||||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -239,11 +326,29 @@ watch(
|
|||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = workingEnv.value?.name ?? null
|
editingName.value = workingEnv.value?.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
|
|
||||||
|
if (props.editingEnvironmentIndex !== "Global") {
|
||||||
|
editingID.value = workingEnv.value?.id ?? uniqueId()
|
||||||
|
}
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
workingEnv.value?.variables ?? [],
|
workingEnv.value?.variables ?? [],
|
||||||
A.map((e) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
props.editingEnvironmentIndex === "Global"
|
||||||
|
? "Global"
|
||||||
|
: workingEnv.value?.id,
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -251,7 +356,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
vars.value = []
|
vars.value = vars.value.filter((e) =>
|
||||||
|
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||||
|
)
|
||||||
|
|
||||||
clearIcon.value = IconDone
|
clearIcon.value = IconDone
|
||||||
toast.success(`${t("state.cleared")}`)
|
toast.success(`${t("state.cleared")}`)
|
||||||
}
|
}
|
||||||
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveEnvironment = () => {
|
const saveEnvironment = () => {
|
||||||
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterdVariables = pipe(
|
const filteredVariables = pipe(
|
||||||
vars.value,
|
vars.value,
|
||||||
A.filterMap(
|
A.filterMap(
|
||||||
flow(
|
flow(
|
||||||
@@ -286,14 +398,43 @@ const saveEnvironment = () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (editingID.value) {
|
||||||
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
editingID.value,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
} else if (props.editingEnvironmentIndex === "Global") {
|
||||||
|
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filteredVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const environmentUpdated: Environment = {
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: uniqueId(),
|
||||||
name: editingName.value,
|
name: editingName.value,
|
||||||
variables: filterdVariables,
|
variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
// Creating a new environment
|
// Creating a new environment
|
||||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
createEnvironment(
|
||||||
|
editingName.value,
|
||||||
|
environmentUpdated.variables,
|
||||||
|
editingID.value
|
||||||
|
)
|
||||||
setSelectedEnvironmentIndex({
|
setSelectedEnvironmentIndex({
|
||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: envList.value.length - 1,
|
index: envList.value.length - 1,
|
||||||
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -150,6 +152,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () => {
|
const exportEnvironmentAsJSON = () => {
|
||||||
const { environment, environmentIndex } = props
|
const { environment, environmentIndex } = props
|
||||||
exportAsJSON(environment, environmentIndex)
|
exportAsJSON(environment, environmentIndex)
|
||||||
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
|
|||||||
if (props.environmentIndex === null) return
|
if (props.environmentIndex === null) return
|
||||||
if (props.environmentIndex !== "Global") {
|
if (props.environmentIndex !== "Global") {
|
||||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:editing-environment-index="editingEnvironmentIndex"
|
:editing-environment-index="editingEnvironmentIndex"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironmentIndex = ref<number | null>(null)
|
const editingEnvironmentIndex = ref<number | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||||
action.value = "new"
|
action.value = "new"
|
||||||
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironmentIndex.value = null
|
editingEnvironmentIndex.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.my.environment.edit",
|
"modals.my.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const envIndex: number = environments.value.findIndex(
|
const envIndex: number = environments.value.findIndex(
|
||||||
(environment: Environment) => {
|
(environment: Environment) => {
|
||||||
return environment.name === envName
|
return environment.name === envName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (envName !== "Global") editEnvironment(envIndex)
|
if (envName !== "Global") {
|
||||||
|
editEnvironment(envIndex)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,90 +16,112 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="my-4 flex flex-col border border-divider rounded">
|
||||||
<label for="variableList" class="p-4">
|
|
||||||
{{ t("environment.variable_list") }}
|
|
||||||
</label>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.clear_all')"
|
|
||||||
:icon="clearIcon"
|
|
||||||
@click="clearContent()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconPlus"
|
|
||||||
:title="t('add.new')"
|
|
||||||
@click="addEnvironmentVariable"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="evnExpandError"
|
|
||||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
|
||||||
>
|
|
||||||
{{ t("environment.nested_overflow") }}
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-dividerLight rounded border border-divider">
|
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-if="evnExpandError"
|
||||||
:key="`variable-${id}-${index}`"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
class="flex divide-x divide-dividerLight"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ t("environment.nested_overflow") }}
|
||||||
v-model="env.key"
|
|
||||||
v-focus
|
|
||||||
class="flex flex-1 bg-transparent px-4 py-2"
|
|
||||||
:class="isViewer && 'opacity-25'"
|
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
|
||||||
:name="'param' + index"
|
|
||||||
:disabled="isViewer"
|
|
||||||
/>
|
|
||||||
<SmartEnvInput
|
|
||||||
v-model="env.value"
|
|
||||||
:select-text-on-mount="env.key === editingVariableName"
|
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
|
||||||
:envs="liveEnvs"
|
|
||||||
:name="'value' + index"
|
|
||||||
:readonly="isViewer"
|
|
||||||
/>
|
|
||||||
<div v-if="!isViewer" class="flex">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
id="variable"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.remove')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
@click="removeEnvironmentVariable(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||||
v-if="vars.length === 0"
|
<template #actions>
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
:alt="`${t('empty.environments')}`"
|
<HoppButtonSecondary
|
||||||
:text="t('empty.environments')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
>
|
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||||
<template #body>
|
blank
|
||||||
<HoppButtonSecondary
|
:title="t('app.wiki')"
|
||||||
v-if="isViewer"
|
:icon="IconHelpCircle"
|
||||||
disabled
|
/>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
v-if="!isViewer"
|
||||||
/>
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
<HoppButtonSecondary
|
:title="t('action.clear_all')"
|
||||||
v-else
|
:icon="clearIcon"
|
||||||
:label="`${t('add.new')}`"
|
@click="clearContent()"
|
||||||
filled
|
/>
|
||||||
@click="addEnvironmentVariable"
|
<HoppButtonSecondary
|
||||||
/>
|
v-if="!isViewer"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:title="t('add.new')"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
<HoppSmartTab
|
||||||
|
v-for="tab in tabsData"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="divide-y divide-dividerLight rounded border border-divider"
|
||||||
|
>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="tab.variables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
:alt="tab.emptyStateLabel"
|
||||||
|
:text="tab.emptyStateLabel"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="!isViewer"
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
filled
|
||||||
|
:icon="IconPlus"
|
||||||
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="({ id, env }, index) in tab.variables"
|
||||||
|
:key="`variable-${id}-${index}`"
|
||||||
|
class="flex divide-x divide-dividerLight"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="env.key"
|
||||||
|
v-focus
|
||||||
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
|
:placeholder="`${t('count.variable', {
|
||||||
|
count: index + 1,
|
||||||
|
})}`"
|
||||||
|
:name="'param' + index"
|
||||||
|
:disabled="isViewer"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="env.value"
|
||||||
|
:select-text-on-mount="
|
||||||
|
env.key ? env.key === editingVariableName : false
|
||||||
|
"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:envs="liveEnvs"
|
||||||
|
:name="'value' + index"
|
||||||
|
:secret="tab.isSecret"
|
||||||
|
:readonly="isViewer && !tab.isSecret"
|
||||||
|
/>
|
||||||
|
<div v-if="!isViewer" class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
id="variable"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="removeEnvironmentVariable(id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</HoppSmartTab>
|
||||||
|
</HoppSmartTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isViewer" #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="`${t('action.save')}`"
|
:label="`${t('action.save')}`"
|
||||||
@@ -119,7 +141,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue"
|
import { ComputedRef, computed, ref, watch } from "vue"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
@@ -141,13 +163,17 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
env: {
|
env: {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
|
secret: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +189,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: string | undefined
|
editingTeamId: string | undefined
|
||||||
editingVariableName?: string | null
|
editingVariableName?: string | null
|
||||||
isViewer?: boolean
|
isViewer?: boolean
|
||||||
|
isSecretOptionSelected?: boolean
|
||||||
envVars?: () => Environment["variables"]
|
envVars?: () => Environment["variables"]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -172,6 +199,7 @@ const props = withDefaults(
|
|||||||
editingTeamId: "",
|
editingTeamId: "",
|
||||||
editingVariableName: null,
|
editingVariableName: null,
|
||||||
isViewer: false,
|
isViewer: false,
|
||||||
|
isSecretOptionSelected: false,
|
||||||
envVars: () => [],
|
envVars: () => [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -182,11 +210,59 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const idTicker = ref(0)
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const tabsData: ComputedRef<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
emptyStateLabel: string
|
||||||
|
isSecret: boolean
|
||||||
|
variables: EnvironmentVariable[]
|
||||||
|
}[]
|
||||||
|
> = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "variables",
|
||||||
|
label: t("environment.variables"),
|
||||||
|
emptyStateLabel: t("empty.environments"),
|
||||||
|
isSecret: false,
|
||||||
|
variables: nonSecretVars.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secret",
|
||||||
|
label: t("environment.secret"),
|
||||||
|
emptyStateLabel: t("empty.secret_environments"),
|
||||||
|
isSecret: true,
|
||||||
|
variables: secretVars.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const editingName = ref<string | null>(null)
|
const editingName = ref<string | null>(null)
|
||||||
|
const editingID = ref<string | null>(null)
|
||||||
const vars = ref<EnvironmentVariable[]>([
|
const vars = ref<EnvironmentVariable[]>([
|
||||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
|
const secretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nonSecretVars = computed(() =>
|
||||||
|
pipe(
|
||||||
|
vars.value,
|
||||||
|
A.filter((e) => !e.env.secret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectedEnv = "variables" | "secret"
|
||||||
|
|
||||||
|
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||||
|
|
||||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||||
IconTrash2,
|
IconTrash2,
|
||||||
1000
|
1000
|
||||||
@@ -215,22 +291,34 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
editingName.value = props.editingEnvironment?.environment.name ?? null
|
||||||
|
selectedEnvOption.value = props.isSecretOptionSelected
|
||||||
|
? "secret"
|
||||||
|
: "variables"
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
editingName.value = null
|
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.envVars() ?? [],
|
props.envVars() ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.map((e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: clone(e),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
} else if (props.editingEnvironment !== null) {
|
} else if (props.editingEnvironment !== null) {
|
||||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
editingID.value = props.editingEnvironment.id
|
||||||
vars.value = pipe(
|
vars.value = pipe(
|
||||||
props.editingEnvironment.environment.variables ?? [],
|
props.editingEnvironment.environment.variables ?? [],
|
||||||
A.map((e: { key: string; value: string }) => ({
|
A.mapWithIndex((index, e) => ({
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
env: clone(e),
|
env: {
|
||||||
|
key: e.key,
|
||||||
|
value: e.secret
|
||||||
|
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
editingID.value ?? "",
|
||||||
|
index
|
||||||
|
)?.value ?? ""
|
||||||
|
: e.value,
|
||||||
|
secret: e.secret,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -250,12 +338,16 @@ const addEnvironmentVariable = () => {
|
|||||||
env: {
|
env: {
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
secret: selectedEnvOption.value === "secret",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEnvironmentVariable = (index: number) => {
|
const removeEnvironmentVariable = (id: number) => {
|
||||||
vars.value.splice(index, 1)
|
const index = vars.value.findIndex((e) => e.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
vars.value.splice(index, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -278,52 +370,102 @@ const saveEnvironment = async () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const secretVariables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.filterMapWithIndex((i, e) =>
|
||||||
|
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const variables = pipe(
|
||||||
|
filterdVariables,
|
||||||
|
A.map((e) =>
|
||||||
|
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const environmentUpdated: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: editingID.value ?? "",
|
||||||
|
name: editingName.value,
|
||||||
|
variables,
|
||||||
|
}
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_CREATE_ENVIRONMENT",
|
type: "HOPP_CREATE_ENVIRONMENT",
|
||||||
workspaceType: "team",
|
workspaceType: "team",
|
||||||
})
|
})
|
||||||
|
|
||||||
await pipe(
|
if (!props.isViewer) {
|
||||||
createTeamEnvironment(
|
await pipe(
|
||||||
JSON.stringify(filterdVariables),
|
createTeamEnvironment(
|
||||||
props.editingTeamId,
|
JSON.stringify(environmentUpdated.variables),
|
||||||
editingName.value
|
props.editingTeamId,
|
||||||
),
|
environmentUpdated.name
|
||||||
TE.match(
|
),
|
||||||
(err: GQLError<string>) => {
|
TE.match(
|
||||||
console.error(err)
|
(err: GQLError<string>) => {
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
console.error(err)
|
||||||
},
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
() => {
|
isLoading.value = false
|
||||||
hideModal()
|
},
|
||||||
toast.success(`${t("environment.created")}`)
|
(res) => {
|
||||||
}
|
const envID = res.createTeamEnvironment.id
|
||||||
)
|
if (envID) {
|
||||||
)()
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
envID,
|
||||||
|
secretVariables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.created")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!props.editingEnvironment) {
|
if (!props.editingEnvironment) {
|
||||||
console.error("No Environment Found")
|
console.error("No Environment Found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipe(
|
if (editingID.value) {
|
||||||
updateTeamEnvironment(
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
JSON.stringify(filterdVariables),
|
editingID.value,
|
||||||
props.editingEnvironment.id,
|
secretVariables
|
||||||
editingName.value
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
hideModal()
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)()
|
|
||||||
|
// If the user is a viewer, we don't need to update the environment in BE
|
||||||
|
// just update the secret environment in the local storage
|
||||||
|
if (props.isViewer) {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isViewer) {
|
||||||
|
await pipe(
|
||||||
|
updateTeamEnvironment(
|
||||||
|
JSON.stringify(environmentUpdated.variables),
|
||||||
|
props.editingEnvironment.id,
|
||||||
|
environmentUpdated.name
|
||||||
|
),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
isLoading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
|
|||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
editingName.value = null
|
||||||
|
selectedEnvOption.value = "variables"
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
v-if="!isViewer"
|
|
||||||
ref="options"
|
ref="options"
|
||||||
interactive
|
interactive
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -57,6 +56,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="duplicate"
|
ref="duplicate"
|
||||||
:icon="IconCopy"
|
:icon="IconCopy"
|
||||||
:label="`${t('action.duplicate')}`"
|
:label="`${t('action.duplicate')}`"
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="exportAsJsonEl"
|
ref="exportAsJsonEl"
|
||||||
:icon="IconEdit"
|
:icon="IconEdit"
|
||||||
:label="`${t('export.as_json')}`"
|
:label="`${t('export.as_json')}`"
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isViewer"
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
:icon="IconTrash2"
|
:icon="IconTrash2"
|
||||||
:label="`${t('action.delete')}`"
|
:label="`${t('action.delete')}`"
|
||||||
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
|||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -137,6 +141,8 @@ const emit = defineEmits<{
|
|||||||
(e: "edit-environment"): void
|
(e: "edit-environment"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
const exportEnvironmentAsJSON = () =>
|
const exportEnvironmentAsJSON = () =>
|
||||||
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
toast.success(`${t("team_environment.deleted")}`)
|
toast.success(`${t("team_environment.deleted")}`)
|
||||||
|
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
:editing-environment="editingEnvironment"
|
:editing-environment="editingEnvironment"
|
||||||
:editing-team-id="team?.id"
|
:editing-team-id="team?.id"
|
||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
|
:is-secret-option-selected="secretOptionSelected"
|
||||||
:is-viewer="team?.myRole === 'VIEWER'"
|
:is-viewer="team?.myRole === 'VIEWER'"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
|
|||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const secretOptionSelected = ref(false)
|
||||||
|
|
||||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||||
|
|
||||||
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
|||||||
}
|
}
|
||||||
const resetSelectedData = () => {
|
const resetSelectedData = () => {
|
||||||
editingEnvironment.value = null
|
editingEnvironment.value = null
|
||||||
|
editingVariableName.value = ""
|
||||||
|
secretOptionSelected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"modals.team.environment.edit",
|
"modals.team.environment.edit",
|
||||||
({ envName, variableName }) => {
|
({ envName, variableName, isSecret }) => {
|
||||||
if (variableName) editingVariableName.value = variableName
|
if (variableName) editingVariableName.value = variableName
|
||||||
const teamEnvToEdit = props.teamEnvironments.find(
|
const teamEnvToEdit = props.teamEnvironments.find(
|
||||||
(environment) => environment.environment.name === envName
|
(environment) => environment.environment.name === envName
|
||||||
)
|
)
|
||||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
if (teamEnvToEdit) {
|
||||||
|
editEnvironment(teamEnvToEdit)
|
||||||
|
secretOptionSelected.value = isSecret ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -187,6 +187,8 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
const requestCode = computed(() => {
|
const requestCode = computed(() => {
|
||||||
const aggregateEnvs = getAggregateEnvs()
|
const aggregateEnvs = getAggregateEnvs()
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
|
v: 1,
|
||||||
|
id: "env",
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: aggregateEnvs,
|
variables: aggregateEnvs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ const clearContent = () => {
|
|||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||||
|
|
||||||
const computedHeaders = computed(() =>
|
const computedHeaders = computed(() =>
|
||||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
getComputedHeaders(request.value, aggregateEnvs.value, false).map(
|
||||||
(header, index) => ({
|
(header, index) => ({
|
||||||
id: `header-${index}`,
|
id: `header-${index}`,
|
||||||
...header,
|
...header,
|
||||||
@@ -606,7 +606,8 @@ const inheritedProperties = computed(() => {
|
|||||||
const computedAuthHeader = getComputedAuthHeaders(
|
const computedAuthHeader = getComputedAuthHeaders(
|
||||||
aggregateEnvs.value,
|
aggregateEnvs.value,
|
||||||
request.value,
|
request.value,
|
||||||
props.inheritedProperties.auth.inheritedAuth
|
props.inheritedProperties.auth.inheritedAuth,
|
||||||
|
false
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const handleAccessTokenRequest = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envs = getCombinedEnvVariables()
|
const envs = getCombinedEnvVariables()
|
||||||
const envVars = [...envs.selected, ...envs.global]
|
const envVars = [...envs.selected.variables, ...envs.global]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenReqParams = {
|
const tokenReqParams = {
|
||||||
|
|||||||
@@ -64,7 +64,6 @@
|
|||||||
:icon="IconShare2"
|
:icon="IconShare2"
|
||||||
:label="t('tab.share_tab_request')"
|
:label="t('tab.share_tab_request')"
|
||||||
:shortcut="['S']"
|
:shortcut="['S']"
|
||||||
:new="true"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
emit('share-tab-request')
|
emit('share-tab-request')
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import {
|
import {
|
||||||
globalEnv$,
|
globalEnv$,
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
setGlobalEnvVariables,
|
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||||
@@ -225,6 +224,7 @@ import { useColorMode } from "~/composables/theming"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppTestResult | null | undefined
|
modelValue: HoppTestResult | null | undefined
|
||||||
@@ -304,9 +304,10 @@ const globalHasAdditions = computed(() => {
|
|||||||
|
|
||||||
const addEnvToGlobal = () => {
|
const addEnvToGlobal = () => {
|
||||||
if (!testResults.value?.envDiff.selected.additions) return
|
if (!testResults.value?.envDiff.selected.additions) return
|
||||||
setGlobalEnvVariables([
|
|
||||||
...globalEnvVars.value,
|
invokeAction("modals.global.environment.update", {
|
||||||
...testResults.value.envDiff.selected.additions,
|
variables: testResults.value.envDiff.selected.additions,
|
||||||
])
|
isSecret: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,18 @@
|
|||||||
<div
|
<div
|
||||||
class="no-scrollbar absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
|
class="no-scrollbar absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="isSecret"
|
||||||
|
id="secret"
|
||||||
|
v-model="secretText"
|
||||||
|
name="secret"
|
||||||
|
:placeholder="t('environment.secret_value')"
|
||||||
|
class="flex flex-1 bg-transparent px-4"
|
||||||
|
:class="styles"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
ref="editor"
|
ref="editor"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="flex flex-1"
|
class="flex flex-1"
|
||||||
@@ -11,7 +22,14 @@
|
|||||||
@click="emit('click', $event)"
|
@click="emit('click', $event)"
|
||||||
@keydown="handleKeystroke"
|
@keydown="handleKeystroke"
|
||||||
@focusin="showSuggestionPopover = true"
|
@focusin="showSuggestionPopover = true"
|
||||||
></div>
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="secret"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="isSecret ? t('action.show_secret') : t('action.hide_secret')"
|
||||||
|
:icon="isSecret ? IconEyeoff : IconEye"
|
||||||
|
@click="toggleSecret"
|
||||||
|
/>
|
||||||
<AppInspection
|
<AppInspection
|
||||||
:inspection-results="inspectionResults"
|
:inspection-results="inspectionResults"
|
||||||
class="sticky inset-y-0 right-0 rounded-r bg-primary"
|
class="sticky inset-y-0 right-0 rounded-r bg-primary"
|
||||||
@@ -61,18 +79,29 @@ import { history, historyKeymap } from "@codemirror/commands"
|
|||||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import {
|
||||||
|
AggregateEnvironment,
|
||||||
|
aggregateEnvsWithSecrets$,
|
||||||
|
} from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
import { InspectorResult } from "~/services/inspection"
|
import { InspectorResult } from "~/services/inspection"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import IconEye from "~icons/lucide/eye"
|
||||||
|
import IconEyeoff from "~icons/lucide/eye-off"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
type Env = Environment["variables"][number] & { source: string }
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
styles?: string
|
styles?: string
|
||||||
envs?: { key: string; value: string; source: string }[] | null
|
envs?: Env[] | null
|
||||||
focus?: boolean
|
focus?: boolean
|
||||||
selectTextOnMount?: boolean
|
selectTextOnMount?: boolean
|
||||||
environmentHighlights?: boolean
|
environmentHighlights?: boolean
|
||||||
@@ -80,6 +109,7 @@ const props = withDefaults(
|
|||||||
autoCompleteSource?: string[]
|
autoCompleteSource?: string[]
|
||||||
inspectionResults?: InspectorResult[] | undefined
|
inspectionResults?: InspectorResult[] | undefined
|
||||||
contextMenuEnabled?: boolean
|
contextMenuEnabled?: boolean
|
||||||
|
secret?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -93,6 +123,7 @@ const props = withDefaults(
|
|||||||
inspectionResult: undefined,
|
inspectionResult: undefined,
|
||||||
inspectionResults: undefined,
|
inspectionResults: undefined,
|
||||||
contextMenuEnabled: true,
|
contextMenuEnabled: true,
|
||||||
|
secret: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,10 +149,27 @@ const showSuggestionPopover = ref(false)
|
|||||||
const suggestionsMenu = ref<any | null>(null)
|
const suggestionsMenu = ref<any | null>(null)
|
||||||
const autoCompleteWrapper = ref<any | null>(null)
|
const autoCompleteWrapper = ref<any | null>(null)
|
||||||
|
|
||||||
|
const isSecret = ref(props.secret)
|
||||||
|
|
||||||
|
const secretText = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => secretText.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (isSecret.value) {
|
||||||
|
updateModelValue(newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onClickOutside(autoCompleteWrapper, () => {
|
onClickOutside(autoCompleteWrapper, () => {
|
||||||
showSuggestionPopover.value = false
|
showSuggestionPopover.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toggleSecret = () => {
|
||||||
|
isSecret.value = !isSecret.value
|
||||||
|
}
|
||||||
|
|
||||||
//filter autocompleteSource with unique values
|
//filter autocompleteSource with unique values
|
||||||
const uniqueAutoCompleteSource = computed(() => {
|
const uniqueAutoCompleteSource = computed(() => {
|
||||||
if (props.autoCompleteSource) {
|
if (props.autoCompleteSource) {
|
||||||
@@ -169,8 +217,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||||
if (!props.autoCompleteSource) return
|
|
||||||
|
|
||||||
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
|
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
}
|
}
|
||||||
@@ -307,19 +353,28 @@ watch(
|
|||||||
let clipboardEv: ClipboardEvent | null = null
|
let clipboardEv: ClipboardEvent | null = null
|
||||||
let pastedValue: string | null = null
|
let pastedValue: string | null = null
|
||||||
|
|
||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
||||||
AggregateEnvironment[]
|
AggregateEnvironment[]
|
||||||
>
|
>
|
||||||
|
|
||||||
const envVars = computed(() =>
|
const envVars = computed(() => {
|
||||||
props.envs
|
return props.envs
|
||||||
? props.envs.map((x) => ({
|
? props.envs.map((x) => {
|
||||||
key: x.key,
|
if (x.secret) {
|
||||||
value: x.value,
|
return {
|
||||||
sourceEnv: x.source,
|
key: x.key,
|
||||||
}))
|
sourceEnv: "source" in x ? x.source : null,
|
||||||
|
value: "********",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: x.key,
|
||||||
|
value: x.value,
|
||||||
|
sourceEnv: "source" in x ? x.source : null,
|
||||||
|
}
|
||||||
|
})
|
||||||
: aggregateEnvs.value
|
: aggregateEnvs.value
|
||||||
)
|
})
|
||||||
|
|
||||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||||
|
|
||||||
@@ -363,17 +418,28 @@ const initView = (el: any) => {
|
|||||||
el.addEventListener("keyup", debounceFn)
|
el.addEventListener("keyup", debounceFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extensions: Extension = getExtensions(props.readonly || isSecret.value)
|
||||||
|
view.value = new EditorView({
|
||||||
|
parent: el,
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: props.modelValue,
|
||||||
|
extensions,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtensions = (readonly: boolean): Extension => {
|
||||||
const extensions: Extension = [
|
const extensions: Extension = [
|
||||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (props.readonly) {
|
if (readonly) {
|
||||||
update.view.contentDOM.inputMode = "none"
|
update.view.contentDOM.inputMode = "none"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorState.changeFilter.of(() => !props.readonly),
|
EditorState.changeFilter.of(() => !readonly),
|
||||||
inputTheme,
|
inputTheme,
|
||||||
props.readonly
|
readonly
|
||||||
? EditorView.theme({
|
? EditorView.theme({
|
||||||
".cm-content": {
|
".cm-content": {
|
||||||
caretColor: "var(--secondary-dark-color)",
|
caretColor: "var(--secondary-dark-color)",
|
||||||
@@ -384,6 +450,7 @@ const initView = (el: any) => {
|
|||||||
})
|
})
|
||||||
: EditorView.theme({}),
|
: EditorView.theme({}),
|
||||||
tooltips({
|
tooltips({
|
||||||
|
parent: document.body,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
}),
|
}),
|
||||||
props.environmentHighlights ? envTooltipPlugin : [],
|
props.environmentHighlights ? envTooltipPlugin : [],
|
||||||
@@ -405,7 +472,8 @@ const initView = (el: any) => {
|
|||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
if (props.readonly) return
|
if (readonly) return
|
||||||
|
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
const prevValue = clone(cachedValue.value)
|
const prevValue = clone(cachedValue.value)
|
||||||
|
|
||||||
@@ -454,14 +522,7 @@ const initView = (el: any) => {
|
|||||||
history(),
|
history(),
|
||||||
keymap.of([...historyKeymap]),
|
keymap.of([...historyKeymap]),
|
||||||
]
|
]
|
||||||
|
return extensions
|
||||||
view.value = new EditorView({
|
|
||||||
parent: el,
|
|
||||||
state: EditorState.create({
|
|
||||||
doc: props.modelValue,
|
|
||||||
extensions,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerTextSelection = () => {
|
const triggerTextSelection = () => {
|
||||||
@@ -474,11 +535,11 @@ const triggerTextSelection = () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (editor.value) {
|
if (editor.value) {
|
||||||
if (!view.value) initView(editor.value)
|
if (!view.value) initView(editor.value)
|
||||||
if (props.selectTextOnMount) triggerTextSelection()
|
if (props.selectTextOnMount) triggerTextSelection()
|
||||||
|
if (props.focus) view.value?.focus()
|
||||||
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
tooltips,
|
||||||
} from "@codemirror/view"
|
} from "@codemirror/view"
|
||||||
import {
|
import {
|
||||||
Extension,
|
Extension,
|
||||||
@@ -269,6 +270,7 @@ export function useCodemirror(
|
|||||||
basicSetup,
|
basicSetup,
|
||||||
baseTheme,
|
baseTheme,
|
||||||
syntaxHighlighting(baseHighlightStyle, { fallback: true }),
|
syntaxHighlighting(baseHighlightStyle, { fallback: true }),
|
||||||
|
|
||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
@@ -318,6 +320,7 @@ export function useCodemirror(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
EditorView.domEventHandlers({
|
EditorView.domEventHandlers({
|
||||||
scroll(event) {
|
scroll(event) {
|
||||||
if (event.target && options.contextMenuEnabled) {
|
if (event.target && options.contextMenuEnabled) {
|
||||||
@@ -359,6 +362,10 @@ export function useCodemirror(
|
|||||||
run: indentLess,
|
run: indentLess,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
tooltips({
|
||||||
|
parent: document.body,
|
||||||
|
position: "absolute",
|
||||||
|
}),
|
||||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||||
additionalExts.of(options.additionalExts ?? []),
|
additionalExts.of(options.additionalExts ?? []),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
|||||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||||
import { isJSONContentType } from "./utils/contenttypes"
|
import { isJSONContentType } from "./utils/contenttypes"
|
||||||
|
import {
|
||||||
|
SecretEnvironmentService,
|
||||||
|
SecretVariable,
|
||||||
|
} from "~/services/secret-environment.service"
|
||||||
|
import { getService } from "~/modules/dioc"
|
||||||
|
|
||||||
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
|
|
||||||
const getTestableBody = (
|
const getTestableBody = (
|
||||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||||
@@ -58,15 +65,63 @@ const getTestableBody = (
|
|||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineEnvVariables = (env: {
|
const combineEnvVariables = (envs: {
|
||||||
global: Environment["variables"]
|
global: Environment["variables"]
|
||||||
selected: Environment["variables"]
|
selected: Environment["variables"]
|
||||||
}) => [...env.selected, ...env.global]
|
}) => [...envs.selected, ...envs.global]
|
||||||
|
|
||||||
export const executedResponses$ = new Subject<
|
export const executedResponses$ = new Subject<
|
||||||
HoppRESTResponse & { type: "success" | "fail " }
|
HoppRESTResponse & { type: "success" | "fail " }
|
||||||
>()
|
>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update the environment schema with the secret variables
|
||||||
|
* and store the secret variable values in the secret environment service
|
||||||
|
* @param envs The environment variables to update
|
||||||
|
* @param type Whether the environment variables are global or selected
|
||||||
|
* @returns the updated environment variables
|
||||||
|
*/
|
||||||
|
const updateEnvironmentsWithSecret = (
|
||||||
|
envs: Environment["variables"] &
|
||||||
|
{
|
||||||
|
secret: true
|
||||||
|
value: string | undefined
|
||||||
|
key: string
|
||||||
|
}[],
|
||||||
|
type: "global" | "selected"
|
||||||
|
) => {
|
||||||
|
const currentEnvID =
|
||||||
|
type === "selected" ? getCurrentEnvironment().id : "Global"
|
||||||
|
|
||||||
|
const updatedSecretEnvironments: SecretVariable[] = []
|
||||||
|
|
||||||
|
const updatedEnv = pipe(
|
||||||
|
envs,
|
||||||
|
A.mapWithIndex((index, e) => {
|
||||||
|
if (e.secret) {
|
||||||
|
updatedSecretEnvironments.push({
|
||||||
|
key: e.key,
|
||||||
|
value: e.value ?? "",
|
||||||
|
varIndex: index,
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete the value from the environment
|
||||||
|
// so that it doesn't get saved in the environment
|
||||||
|
delete e.value
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (currentEnvID) {
|
||||||
|
secretEnvironmentService.addSecretEnvironment(
|
||||||
|
currentEnvID,
|
||||||
|
updatedSecretEnvironments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return updatedEnv
|
||||||
|
}
|
||||||
|
|
||||||
export function runRESTRequest$(
|
export function runRESTRequest$(
|
||||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||||
): [
|
): [
|
||||||
@@ -154,15 +209,36 @@ export function runRESTRequest$(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (E.isRight(runResult)) {
|
if (E.isRight(runResult)) {
|
||||||
|
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
|
||||||
|
cloneDeep(runResult.right.envs.global),
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
|
||||||
|
cloneDeep(runResult.right.envs.selected),
|
||||||
|
"selected"
|
||||||
|
)
|
||||||
|
|
||||||
// set the response in the tab so that multiple tabs can run request simultaneously
|
// set the response in the tab so that multiple tabs can run request simultaneously
|
||||||
tab.value.document.response = res
|
tab.value.document.response = res
|
||||||
|
|
||||||
tab.value.document.testResults = translateToSandboxTestResults(
|
const updatedRunResult = {
|
||||||
runResult.right
|
...runResult.right,
|
||||||
|
envs: {
|
||||||
|
global: updatedGlobalEnvVariables,
|
||||||
|
selected: updatedSelectedEnvVariables,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.value.document.testResults =
|
||||||
|
translateToSandboxTestResults(updatedRunResult)
|
||||||
|
|
||||||
|
setGlobalEnvVariables(
|
||||||
|
updateEnvironmentsWithSecret(
|
||||||
|
runResult.right.envs.global,
|
||||||
|
"global"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
setGlobalEnvVariables(runResult.right.envs.global)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||||
) {
|
) {
|
||||||
@@ -173,8 +249,10 @@ export function runRESTRequest$(
|
|||||||
updateEnvironment(
|
updateEnvironment(
|
||||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||||
{
|
{
|
||||||
...env,
|
name: env.name,
|
||||||
variables: runResult.right.envs.selected,
|
v: 1,
|
||||||
|
id: env.id ?? "",
|
||||||
|
variables: updatedRunResult.envs.selected,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
@@ -186,7 +264,7 @@ export function runRESTRequest$(
|
|||||||
})
|
})
|
||||||
pipe(
|
pipe(
|
||||||
updateTeamEnvironment(
|
updateTeamEnvironment(
|
||||||
JSON.stringify(runResult.right.envs.selected),
|
JSON.stringify(updatedRunResult.envs.selected),
|
||||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||||
env.name
|
env.name
|
||||||
)
|
)
|
||||||
@@ -275,7 +353,6 @@ function translateToSandboxTestResults(
|
|||||||
|
|
||||||
const globals = cloneDeep(getGlobalVariables())
|
const globals = cloneDeep(getGlobalVariables())
|
||||||
const env = getCurrentEnvironment()
|
const env = getCurrentEnvironment()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: "",
|
description: "",
|
||||||
expectResults: testDesc.tests.expectResults,
|
expectResults: testDesc.tests.expectResults,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
||||||
import { BehaviorSubject } from "rxjs"
|
import { BehaviorSubject } from "rxjs"
|
||||||
import { HoppRESTDocument } from "./rest/document"
|
import { HoppRESTDocument } from "./rest/document"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||||
import { HoppGQLSaveContext } from "./graphql/document"
|
import { HoppGQLSaveContext } from "./graphql/document"
|
||||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||||
@@ -43,6 +43,7 @@ export type HoppAction =
|
|||||||
| "modals.environment.new" // Add new environment
|
| "modals.environment.new" // Add new environment
|
||||||
| "modals.environment.delete-selected" // Delete Selected Environment
|
| "modals.environment.delete-selected" // Delete Selected Environment
|
||||||
| "modals.my.environment.edit" // Edit current personal environment
|
| "modals.my.environment.edit" // Edit current personal environment
|
||||||
|
| "modals.global.environment.update" // Update global environment
|
||||||
| "modals.team.environment.edit" // Edit current team environment
|
| "modals.team.environment.edit" // Edit current team environment
|
||||||
| "modals.team.new" // Add new team
|
| "modals.team.new" // Add new team
|
||||||
| "modals.team.edit" // Edit selected team
|
| "modals.team.edit" // Edit selected team
|
||||||
@@ -93,13 +94,19 @@ type HoppActionArgsMap = {
|
|||||||
}
|
}
|
||||||
text: string | null
|
text: string | null
|
||||||
}
|
}
|
||||||
|
"modals.global.environment.update": {
|
||||||
|
variables?: Environment["variables"]
|
||||||
|
isSecret?: boolean
|
||||||
|
}
|
||||||
"modals.my.environment.edit": {
|
"modals.my.environment.edit": {
|
||||||
envName: string
|
envName: string
|
||||||
variableName?: string
|
variableName?: string
|
||||||
|
isSecret?: boolean
|
||||||
}
|
}
|
||||||
"modals.team.environment.edit": {
|
"modals.team.environment.edit": {
|
||||||
envName: string
|
envName: string
|
||||||
variableName?: string
|
variableName?: string
|
||||||
|
isSecret?: boolean
|
||||||
}
|
}
|
||||||
"modals.team.delete": {
|
"modals.team.delete": {
|
||||||
teamId: string
|
teamId: string
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
|
mutation CreateTeamEnvironment(
|
||||||
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
|
$variables: String!
|
||||||
|
$teamID: ID!
|
||||||
|
$name: String!
|
||||||
|
) {
|
||||||
|
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
|
||||||
variables
|
variables
|
||||||
name
|
name
|
||||||
teamID
|
teamID
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { parseTemplateStringE } from "@hoppscotch/data"
|
|||||||
import { StreamSubscriberFunc } from "@composables/stream"
|
import { StreamSubscriberFunc } from "@composables/stream"
|
||||||
import {
|
import {
|
||||||
AggregateEnvironment,
|
AggregateEnvironment,
|
||||||
aggregateEnvs$,
|
aggregateEnvsWithSecrets$,
|
||||||
getAggregateEnvs,
|
getAggregateEnvsWithSecrets,
|
||||||
getSelectedEnvironmentType,
|
getSelectedEnvironmentType,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
@@ -66,7 +66,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
|
|
||||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||||
|
|
||||||
const envValue = tooltipEnv?.value ?? "Not found"
|
let envValue = "Not Found"
|
||||||
|
|
||||||
|
if (!tooltipEnv?.secret && tooltipEnv?.value) envValue = tooltipEnv.value
|
||||||
|
else if (tooltipEnv?.secret && tooltipEnv.value) {
|
||||||
|
envValue = "******"
|
||||||
|
} else if (!tooltipEnv?.sourceEnv) {
|
||||||
|
envValue = "Not Found"
|
||||||
|
} else if (!tooltipEnv?.value) {
|
||||||
|
envValue = "Empty"
|
||||||
|
}
|
||||||
|
|
||||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||||
|
|
||||||
@@ -89,6 +98,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
invokeAction(`modals.${action}.environment.edit`, {
|
invokeAction(`modals.${action}.environment.edit`, {
|
||||||
envName,
|
envName,
|
||||||
variableName: parsedEnvKey,
|
variableName: parsedEnvKey,
|
||||||
|
isSecret: tooltipEnv?.secret,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||||
@@ -171,9 +181,9 @@ export class HoppEnvironmentPlugin {
|
|||||||
subscribeToStream: StreamSubscriberFunc,
|
subscribeToStream: StreamSubscriberFunc,
|
||||||
private editorView: Ref<EditorView | undefined>
|
private editorView: Ref<EditorView | undefined>
|
||||||
) {
|
) {
|
||||||
this.envs = getAggregateEnvs()
|
this.envs = getAggregateEnvsWithSecrets()
|
||||||
|
|
||||||
subscribeToStream(aggregateEnvs$, (envs) => {
|
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||||
this.envs = envs
|
this.envs = envs
|
||||||
|
|
||||||
this.editorView.value?.dispatch({
|
this.editorView.value?.dispatch({
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ const getEnvironmentJson = (
|
|||||||
? cloneDeep(environmentObj.environment)
|
? cloneDeep(environmentObj.environment)
|
||||||
: cloneDeep(environmentObj)
|
: cloneDeep(environmentObj)
|
||||||
|
|
||||||
delete newEnvironment.id
|
|
||||||
|
|
||||||
const environmentId =
|
const environmentId =
|
||||||
environmentIndex || environmentIndex === 0
|
environmentIndex || environmentIndex === 0
|
||||||
? environmentIndex
|
? environmentIndex
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { entityReference } from "verzod"
|
||||||
|
|
||||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
|
||||||
import { safeParseJSON } from "~/helpers/functional/json"
|
import { safeParseJSON } from "~/helpers/functional/json"
|
||||||
|
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
|
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
const hoppEnvSchema = z.object({
|
|
||||||
id: z.string().optional(),
|
|
||||||
name: z.string(),
|
|
||||||
variables: z.array(
|
|
||||||
z.object({
|
|
||||||
key: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const hoppEnvImporter = (content: string) => {
|
export const hoppEnvImporter = (content: string) => {
|
||||||
const parsedContent = safeParseJSON(content, true)
|
const parsedContent = safeParseJSON(content, true)
|
||||||
|
|
||||||
@@ -25,7 +16,9 @@ export const hoppEnvImporter = (content: string) => {
|
|||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value)
|
const validationResult = z
|
||||||
|
.array(entityReference(Environment))
|
||||||
|
.safeParse(parsedContent.value)
|
||||||
|
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import * as O from "fp-ts/Option"
|
|||||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { NonSecretEnvironment } from "@hoppscotch/data"
|
||||||
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
||||||
|
import { uniqueId } from "lodash-es"
|
||||||
|
|
||||||
const insomniaResourcesSchema = z.object({
|
const insomniaResourcesSchema = z.object({
|
||||||
resources: z.array(
|
resources: z.array(
|
||||||
@@ -56,16 +57,18 @@ export const insomniaEnvImporter = (content: string) => {
|
|||||||
return { ...envResource, data: stringifiedData }
|
return { ...envResource, data: stringifiedData }
|
||||||
})
|
})
|
||||||
|
|
||||||
const environments: Environment[] = []
|
const environments: NonSecretEnvironment[] = []
|
||||||
|
|
||||||
insomniaEnvs.forEach((insomniaEnv) => {
|
insomniaEnvs.forEach((insomniaEnv) => {
|
||||||
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
||||||
|
|
||||||
if (parsedInsomniaEnv.success) {
|
if (parsedInsomniaEnv.success) {
|
||||||
const environment: Environment = {
|
const environment: NonSecretEnvironment = {
|
||||||
|
id: uniqueId(),
|
||||||
|
v: 1,
|
||||||
name: parsedInsomniaEnv.data.name,
|
name: parsedInsomniaEnv.data.name,
|
||||||
variables: Object.entries(parsedInsomniaEnv.data.data).map(
|
variables: Object.entries(parsedInsomniaEnv.data.data).map(
|
||||||
([key, value]) => ({ key, value })
|
([key, value]) => ({ key, value, secret: false })
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { safeParseJSON } from "~/helpers/functional/json"
|
|||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
import { uniqueId } from "lodash-es"
|
||||||
|
|
||||||
const postmanEnvSchema = z.object({
|
const postmanEnvSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -34,12 +35,14 @@ export const postmanEnvImporter = (content: string) => {
|
|||||||
const postmanEnv = validationResult.data
|
const postmanEnv = validationResult.data
|
||||||
|
|
||||||
const environment: Environment = {
|
const environment: Environment = {
|
||||||
|
id: uniqueId(),
|
||||||
|
v: 1,
|
||||||
name: postmanEnv.name,
|
name: postmanEnv.name,
|
||||||
variables: [],
|
variables: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
postmanEnv.values.forEach(({ key, value }) =>
|
postmanEnv.values.forEach(({ key, value }) =>
|
||||||
environment.variables.push({ key, value })
|
environment.variables.push({ key, value, secret: false })
|
||||||
)
|
)
|
||||||
|
|
||||||
return TE.right(environment)
|
return TE.right(environment)
|
||||||
|
|||||||
@@ -8,11 +8,71 @@ import {
|
|||||||
getGlobalVariables,
|
getGlobalVariables,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||||
|
import { getService } from "~/modules/dioc"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
export const getCombinedEnvVariables = () => ({
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
global: cloneDeep(getGlobalVariables()),
|
|
||||||
selected: cloneDeep(getCurrentEnvironment().variables),
|
const unsecretEnvironments = (
|
||||||
})
|
global: Environment["variables"],
|
||||||
|
selected: Environment
|
||||||
|
) => {
|
||||||
|
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
||||||
|
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
"Global",
|
||||||
|
index
|
||||||
|
)
|
||||||
|
if (secretVar) {
|
||||||
|
return {
|
||||||
|
...globalVar,
|
||||||
|
value: secretVar.value,
|
||||||
|
}
|
||||||
|
} else if (!("value" in globalVar) || !globalVar.value) {
|
||||||
|
return {
|
||||||
|
...globalVar,
|
||||||
|
value: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalVar
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedSelectedWithSecrets = selected.variables.map(
|
||||||
|
(selectedVar, index) => {
|
||||||
|
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
|
selected.id,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
if (secretVar) {
|
||||||
|
return {
|
||||||
|
...selectedVar,
|
||||||
|
value: secretVar.value,
|
||||||
|
}
|
||||||
|
} else if (!("value" in selectedVar) || !selectedVar.value) {
|
||||||
|
return {
|
||||||
|
...selectedVar,
|
||||||
|
value: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedVar
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
global: resolvedGlobalWithSecrets,
|
||||||
|
selected: resolvedSelectedWithSecrets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCombinedEnvVariables = () => {
|
||||||
|
const reformedVars = unsecretEnvironments(
|
||||||
|
getGlobalVariables(),
|
||||||
|
getCurrentEnvironment()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
global: cloneDeep(reformedVars.global),
|
||||||
|
selected: cloneDeep(reformedVars.selected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getFinalEnvsFromPreRequest = (
|
export const getFinalEnvsFromPreRequest = (
|
||||||
script: string,
|
script: string,
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ export default class TeamEnvironmentAdapter {
|
|||||||
id: x.id,
|
id: x.id,
|
||||||
teamID: x.teamID,
|
teamID: x.teamID,
|
||||||
environment: {
|
environment: {
|
||||||
|
v: 1,
|
||||||
|
id: x.id,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
variables: JSON.parse(x.variables),
|
variables: JSON.parse(x.variables),
|
||||||
},
|
},
|
||||||
@@ -196,6 +198,8 @@ export default class TeamEnvironmentAdapter {
|
|||||||
id: x.id,
|
id: x.id,
|
||||||
teamID: x.teamID,
|
teamID: x.teamID,
|
||||||
environment: {
|
environment: {
|
||||||
|
v: 1,
|
||||||
|
id: x.id,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
variables: JSON.parse(x.variables),
|
variables: JSON.parse(x.variables),
|
||||||
},
|
},
|
||||||
@@ -249,6 +253,8 @@ export default class TeamEnvironmentAdapter {
|
|||||||
id: x.id,
|
id: x.id,
|
||||||
teamID: x.teamID,
|
teamID: x.teamID,
|
||||||
environment: {
|
environment: {
|
||||||
|
v: 1,
|
||||||
|
id: x.id,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
variables: JSON.parse(x.variables),
|
variables: JSON.parse(x.variables),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
|||||||
export const getComputedAuthHeaders = (
|
export const getComputedAuthHeaders = (
|
||||||
envVars: Environment["variables"],
|
envVars: Environment["variables"],
|
||||||
req?: HoppRESTRequest,
|
req?: HoppRESTRequest,
|
||||||
auth?: HoppRESTRequest["auth"]
|
auth?: HoppRESTRequest["auth"],
|
||||||
|
parse = true
|
||||||
) => {
|
) => {
|
||||||
const request = auth ? { auth: auth ?? { authActive: false } } : req
|
const request = auth ? { auth: auth ?? { authActive: false } } : req
|
||||||
// If Authorization header is also being user-defined, that takes priority
|
// If Authorization header is also being user-defined, that takes priority
|
||||||
@@ -60,8 +61,12 @@ export const getComputedAuthHeaders = (
|
|||||||
|
|
||||||
// TODO: Support a better b64 implementation than btoa ?
|
// TODO: Support a better b64 implementation than btoa ?
|
||||||
if (request.auth.authType === "basic") {
|
if (request.auth.authType === "basic") {
|
||||||
const username = parseTemplateString(request.auth.username, envVars)
|
const username = parse
|
||||||
const password = parseTemplateString(request.auth.password, envVars)
|
? parseTemplateString(request.auth.username, envVars)
|
||||||
|
: request.auth.username
|
||||||
|
const password = parse
|
||||||
|
? parseTemplateString(request.auth.password, envVars)
|
||||||
|
: request.auth.password
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
active: true,
|
active: true,
|
||||||
@@ -75,7 +80,11 @@ export const getComputedAuthHeaders = (
|
|||||||
headers.push({
|
headers.push({
|
||||||
active: true,
|
active: true,
|
||||||
key: "Authorization",
|
key: "Authorization",
|
||||||
value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`,
|
value: `Bearer ${
|
||||||
|
parse
|
||||||
|
? parseTemplateString(request.auth.token, envVars)
|
||||||
|
: request.auth.token
|
||||||
|
}`,
|
||||||
})
|
})
|
||||||
} else if (request.auth.authType === "api-key") {
|
} else if (request.auth.authType === "api-key") {
|
||||||
const { key, addTo } = request.auth
|
const { key, addTo } = request.auth
|
||||||
@@ -83,7 +92,9 @@ export const getComputedAuthHeaders = (
|
|||||||
headers.push({
|
headers.push({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(key, envVars),
|
key: parseTemplateString(key, envVars),
|
||||||
value: parseTemplateString(request.auth.value ?? "", envVars),
|
value: parse
|
||||||
|
? parseTemplateString(request.auth.value ?? "", envVars)
|
||||||
|
: request.auth.value ?? "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,10 +144,11 @@ export type ComputedHeader = {
|
|||||||
*/
|
*/
|
||||||
export const getComputedHeaders = (
|
export const getComputedHeaders = (
|
||||||
req: HoppRESTRequest,
|
req: HoppRESTRequest,
|
||||||
envVars: Environment["variables"]
|
envVars: Environment["variables"],
|
||||||
|
parse = true
|
||||||
): ComputedHeader[] => {
|
): ComputedHeader[] => {
|
||||||
return [
|
return [
|
||||||
...getComputedAuthHeaders(envVars, req).map((header) => ({
|
...getComputedAuthHeaders(envVars, req, undefined, parse).map((header) => ({
|
||||||
source: "auth" as const,
|
source: "auth" as const,
|
||||||
header,
|
header,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { cloneDeep, isEqual } from "lodash-es"
|
import { cloneDeep, isEqual, uniqueId } from "lodash-es"
|
||||||
import { combineLatest, Observable } from "rxjs"
|
import { combineLatest, Observable } from "rxjs"
|
||||||
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
|
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
|
||||||
|
import { getService } from "~/modules/dioc"
|
||||||
import DispatchingStore, {
|
import DispatchingStore, {
|
||||||
defineDispatchers,
|
defineDispatchers,
|
||||||
} from "~/newstore/DispatchingStore"
|
} from "~/newstore/DispatchingStore"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
export type SelectedEnvironmentIndex =
|
export type SelectedEnvironmentIndex =
|
||||||
| { type: "NO_ENV_SELECTED" }
|
| { type: "NO_ENV_SELECTED" }
|
||||||
@@ -19,6 +21,8 @@ export type SelectedEnvironmentIndex =
|
|||||||
const defaultEnvironmentsState = {
|
const defaultEnvironmentsState = {
|
||||||
environments: [
|
environments: [
|
||||||
{
|
{
|
||||||
|
v: 1,
|
||||||
|
id: uniqueId(),
|
||||||
name: "My Environment Variables",
|
name: "My Environment Variables",
|
||||||
variables: [],
|
variables: [],
|
||||||
},
|
},
|
||||||
@@ -33,6 +37,8 @@ const defaultEnvironmentsState = {
|
|||||||
} as SelectedEnvironmentIndex,
|
} as SelectedEnvironmentIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
|
|
||||||
type EnvironmentStore = typeof defaultEnvironmentsState
|
type EnvironmentStore = typeof defaultEnvironmentsState
|
||||||
|
|
||||||
const dispatchers = defineDispatchers({
|
const dispatchers = defineDispatchers({
|
||||||
@@ -88,10 +94,13 @@ const dispatchers = defineDispatchers({
|
|||||||
envID
|
envID
|
||||||
? {
|
? {
|
||||||
id: envID,
|
id: envID,
|
||||||
|
v: 1,
|
||||||
name,
|
name,
|
||||||
variables,
|
variables,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
v: 1,
|
||||||
|
id: "",
|
||||||
name,
|
name,
|
||||||
variables,
|
variables,
|
||||||
},
|
},
|
||||||
@@ -109,14 +118,12 @@ const dispatchers = defineDispatchers({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the id, because this is a new environment & it will get its own id when syncing
|
|
||||||
delete newEnvironment["id"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
environments: [
|
environments: [
|
||||||
...environments,
|
...environments,
|
||||||
{
|
{
|
||||||
...cloneDeep(newEnvironment),
|
...cloneDeep(newEnvironment),
|
||||||
|
id: uniqueId(),
|
||||||
name: `${newEnvironment.name} - Duplicate`,
|
name: `${newEnvironment.name} - Duplicate`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -184,14 +191,19 @@ const dispatchers = defineDispatchers({
|
|||||||
},
|
},
|
||||||
addEnvironmentVariable(
|
addEnvironmentVariable(
|
||||||
{ environments }: EnvironmentStore,
|
{ environments }: EnvironmentStore,
|
||||||
{ envIndex, key, value }: { envIndex: number; key: string; value: string }
|
{
|
||||||
|
envIndex,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
secret,
|
||||||
|
}: { envIndex: number; key: string; value: string; secret: boolean }
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
environments: environments.map((env, index) =>
|
environments: environments.map((env, index) =>
|
||||||
index === envIndex
|
index === envIndex
|
||||||
? {
|
? {
|
||||||
...env,
|
...env,
|
||||||
variables: [...env.variables, { key, value }],
|
variables: [...env.variables, { key, value, secret }],
|
||||||
}
|
}
|
||||||
: env
|
: env
|
||||||
),
|
),
|
||||||
@@ -219,7 +231,10 @@ const dispatchers = defineDispatchers({
|
|||||||
{
|
{
|
||||||
envIndex,
|
envIndex,
|
||||||
vars,
|
vars,
|
||||||
}: { envIndex: number; vars: { key: string; value: string }[] }
|
}: {
|
||||||
|
envIndex: number
|
||||||
|
vars: { key: string; value: string; secret: boolean }[]
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
environments: environments.map((env, index) =>
|
environments: environments.map((env, index) =>
|
||||||
@@ -253,7 +268,7 @@ const dispatchers = defineDispatchers({
|
|||||||
...env,
|
...env,
|
||||||
variables: env.variables.map((v, vIndex) =>
|
variables: env.variables.map((v, vIndex) =>
|
||||||
vIndex === variableIndex
|
vIndex === variableIndex
|
||||||
? { key: updatedKey, value: updatedValue }
|
? { key: updatedKey, value: updatedValue, secret: v.secret }
|
||||||
: v
|
: v
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -343,6 +358,8 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
|||||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
name: "No environment",
|
name: "No environment",
|
||||||
|
v: 1,
|
||||||
|
id: "",
|
||||||
variables: [],
|
variables: [],
|
||||||
}
|
}
|
||||||
return env
|
return env
|
||||||
@@ -356,6 +373,7 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
|||||||
export type AggregateEnvironment = {
|
export type AggregateEnvironment = {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
|
secret: boolean
|
||||||
sourceEnv: string
|
sourceEnv: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,11 +388,11 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
|||||||
map(([selectedEnv, globalVars]) => {
|
map(([selectedEnv, globalVars]) => {
|
||||||
const results: AggregateEnvironment[] = []
|
const results: AggregateEnvironment[] = []
|
||||||
|
|
||||||
selectedEnv?.variables.forEach(({ key, value }) =>
|
selectedEnv?.variables.forEach(({ key, value, secret }) =>
|
||||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
results.push({ key, value, secret, sourceEnv: selectedEnv.name })
|
||||||
)
|
)
|
||||||
globalVars.forEach(({ key, value }) =>
|
globalVars.forEach(({ key, value, secret }) =>
|
||||||
results.push({ key, value, sourceEnv: "Global" })
|
results.push({ key, value, secret, sourceEnv: "Global" })
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -384,32 +402,129 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
|||||||
|
|
||||||
export function getAggregateEnvs() {
|
export function getAggregateEnvs() {
|
||||||
const currentEnv = getCurrentEnvironment()
|
const currentEnv = getCurrentEnvironment()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...currentEnv.variables.map(
|
...currentEnv.variables.map((x) => {
|
||||||
(x) =>
|
let value
|
||||||
<AggregateEnvironment>{
|
if (!x.secret) {
|
||||||
key: x.key,
|
value = x.value
|
||||||
value: x.value,
|
}
|
||||||
sourceEnv: currentEnv.name,
|
|
||||||
}
|
return <AggregateEnvironment>{
|
||||||
),
|
key: x.key,
|
||||||
...getGlobalVariables().map(
|
value,
|
||||||
(x) =>
|
secret: x.secret,
|
||||||
<AggregateEnvironment>{
|
sourceEnv: currentEnv.name,
|
||||||
key: x.key,
|
}
|
||||||
value: x.value,
|
}),
|
||||||
sourceEnv: "Global",
|
...getGlobalVariables().map((x) => {
|
||||||
}
|
let value
|
||||||
),
|
if (!x.secret) {
|
||||||
|
value = x.value
|
||||||
|
}
|
||||||
|
return <AggregateEnvironment>{
|
||||||
|
key: x.key,
|
||||||
|
value,
|
||||||
|
secret: x.secret,
|
||||||
|
sourceEnv: "Global",
|
||||||
|
}
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAggregateEnvsWithSecrets() {
|
||||||
|
const currentEnv = getCurrentEnvironment()
|
||||||
|
return [
|
||||||
|
...currentEnv.variables.map((x, index) => {
|
||||||
|
let value
|
||||||
|
if (x.secret) {
|
||||||
|
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
|
||||||
|
currentEnv.id,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
value = x.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AggregateEnvironment>{
|
||||||
|
key: x.key,
|
||||||
|
value,
|
||||||
|
secret: x.secret,
|
||||||
|
sourceEnv: currentEnv.name,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...getGlobalVariables().map((x, index) => {
|
||||||
|
let value
|
||||||
|
if (x.secret) {
|
||||||
|
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
|
||||||
|
"Global",
|
||||||
|
index
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
value = x.value
|
||||||
|
}
|
||||||
|
return <AggregateEnvironment>{
|
||||||
|
key: x.key,
|
||||||
|
value,
|
||||||
|
secret: x.secret,
|
||||||
|
sourceEnv: "Global",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
|
||||||
|
combineLatest([currentEnvironment$, globalEnv$]).pipe(
|
||||||
|
map(([selectedEnv, globalVars]) => {
|
||||||
|
const results: AggregateEnvironment[] = []
|
||||||
|
|
||||||
|
selectedEnv?.variables.map((x, index) => {
|
||||||
|
let value
|
||||||
|
if (x.secret) {
|
||||||
|
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
|
||||||
|
selectedEnv.id,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
value = x.value
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
key: x.key,
|
||||||
|
value: value ?? "",
|
||||||
|
secret: x.secret,
|
||||||
|
sourceEnv: selectedEnv.name,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
globalVars.map((x, index) => {
|
||||||
|
let value
|
||||||
|
if (x.secret) {
|
||||||
|
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
|
||||||
|
"Global",
|
||||||
|
index
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
value = x.value
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
key: x.key,
|
||||||
|
value: value ?? "",
|
||||||
|
secret: x.secret,
|
||||||
|
sourceEnv: "Global",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(isEqual)
|
||||||
|
)
|
||||||
|
|
||||||
export function getCurrentEnvironment(): Environment {
|
export function getCurrentEnvironment(): Environment {
|
||||||
if (
|
if (
|
||||||
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
|
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
v: 1,
|
||||||
|
id: "",
|
||||||
name: "No environment",
|
name: "No environment",
|
||||||
variables: [],
|
variables: [],
|
||||||
}
|
}
|
||||||
@@ -589,7 +704,7 @@ export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
|
|||||||
|
|
||||||
export function setEnvironmentVariables(
|
export function setEnvironmentVariables(
|
||||||
envIndex: number,
|
envIndex: number,
|
||||||
vars: { key: string; value: string }[]
|
vars: { key: string; value: string; secret: boolean }[]
|
||||||
) {
|
) {
|
||||||
environmentsStore.dispatch({
|
environmentsStore.dispatch({
|
||||||
dispatcher: "setEnvironmentVariables",
|
dispatcher: "setEnvironmentVariables",
|
||||||
@@ -602,7 +717,7 @@ export function setEnvironmentVariables(
|
|||||||
|
|
||||||
export function addEnvironmentVariable(
|
export function addEnvironmentVariable(
|
||||||
envIndex: number,
|
envIndex: number,
|
||||||
{ key, value }: { key: string; value: string }
|
{ key, value, secret }: { key: string; value: string; secret: boolean }
|
||||||
) {
|
) {
|
||||||
environmentsStore.dispatch({
|
environmentsStore.dispatch({
|
||||||
dispatcher: "addEnvironmentVariable",
|
dispatcher: "addEnvironmentVariable",
|
||||||
@@ -610,6 +725,7 @@ export function addEnvironmentVariable(
|
|||||||
envIndex,
|
envIndex,
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
|
secret,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { describe, expect, it, beforeEach } from "vitest"
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { SecretEnvironmentService } from "../secret-environment.service"
|
||||||
|
|
||||||
|
describe("SecretEnvironmentService", () => {
|
||||||
|
let container: TestContainer
|
||||||
|
let service: SecretEnvironmentService
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = new TestContainer()
|
||||||
|
service = container.bind(SecretEnvironmentService)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("addSecretEnvironment", () => {
|
||||||
|
it("should add a new secret environment with the provided ID and secret variables", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.addSecretEnvironment(id, secretVars)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.get(id)).toEqual(secretVars)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getSecretEnvironment", () => {
|
||||||
|
it("should return the secret variables of the specified environment", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
expect(service.getSecretEnvironment(id)).toEqual(secretVars)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if the specified environment does not exist", () => {
|
||||||
|
const id = "nonExistentEnvironment"
|
||||||
|
|
||||||
|
expect(service.getSecretEnvironment(id)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getSecretEnvironmentVariable", () => {
|
||||||
|
it("should return the specified secret environment variable", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariable(id, 1)
|
||||||
|
|
||||||
|
expect(result).toEqual(secretVars[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if the specified variable does not exist", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariable(id, 2)
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if the specified environment does not exist", () => {
|
||||||
|
const id = "nonExistentEnvironment"
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariable(id, 1)
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getSecretEnvironmentVariableValue", () => {
|
||||||
|
it("should return the value of the specified secret environment variable", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariableValue(id, 1)
|
||||||
|
|
||||||
|
expect(result).toEqual(secretVars[0].value)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if the specified variable does not exist", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariableValue(id, 2)
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if the specified environment does not exist", () => {
|
||||||
|
const id = "nonExistentEnvironment"
|
||||||
|
|
||||||
|
const result = service.getSecretEnvironmentVariableValue(id, 1)
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("loadSecretEnvironmentsFromPersistedState", () => {
|
||||||
|
it("should load secret environments from the persisted state", () => {
|
||||||
|
const persistedState = {
|
||||||
|
testEnvironment: [{ key: "key1", value: "value1", varIndex: 1 }],
|
||||||
|
}
|
||||||
|
|
||||||
|
service.loadSecretEnvironmentsFromPersistedState(persistedState)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.size).toBe(1)
|
||||||
|
expect(service.secretEnvironments.get("testEnvironment")).toEqual([
|
||||||
|
{ key: "key1", value: "value1", varIndex: 1 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("deleteSecretEnvironment", () => {
|
||||||
|
it("should delete the specified secret environment", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
service.deleteSecretEnvironment(id)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.has(id)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("removeSecretEnvironmentVariable", () => {
|
||||||
|
it("should remove the specified secret environment variable", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [
|
||||||
|
{ key: "key1", value: "value1", varIndex: 1 },
|
||||||
|
{ key: "key2", value: "value2", varIndex: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
service.removeSecretEnvironmentVariable(id, 1)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.get(id)).toEqual([
|
||||||
|
{ key: "key2", value: "value2", varIndex: 2 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should do nothing if the specified variable does not exist", () => {
|
||||||
|
const id = "testEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(id, secretVars)
|
||||||
|
|
||||||
|
service.removeSecretEnvironmentVariable(id, 2)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.get(id)).toEqual(secretVars)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateSecretEnvironmentID", () => {
|
||||||
|
it("should update the ID of the specified secret environment", () => {
|
||||||
|
const oldID = "oldEnvironment"
|
||||||
|
const newID = "newEnvironment"
|
||||||
|
const secretVars = [{ key: "key1", value: "value1", varIndex: 1 }]
|
||||||
|
|
||||||
|
service.secretEnvironments.set(oldID, secretVars)
|
||||||
|
|
||||||
|
service.updateSecretEnvironmentID(oldID, newID)
|
||||||
|
|
||||||
|
expect(service.secretEnvironments.has(oldID)).toBe(false)
|
||||||
|
expect(service.secretEnvironments.get(newID)).toEqual(secretVars)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("persistableSecretEnvironments", () => {
|
||||||
|
it("should return a record of secret environments suitable for persistence", () => {
|
||||||
|
const secretVars = [
|
||||||
|
{ key: "key1", value: "value1", varIndex: 1 },
|
||||||
|
{ key: "key2", value: "value2", varIndex: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
service.secretEnvironments.set("environment1", secretVars)
|
||||||
|
|
||||||
|
const persistedState = service.persistableSecretEnvironments.value
|
||||||
|
|
||||||
|
expect(persistedState).toEqual({
|
||||||
|
environment1: secretVars,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -15,9 +15,21 @@ vi.mock("~/newstore/environments", async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
aggregateEnvs$: new BehaviorSubject([
|
aggregateEnvsWithSecrets$: new BehaviorSubject([
|
||||||
{ key: "EXISTING_ENV_VAR", value: "test_value" },
|
{ key: "EXISTING_ENV_VAR", value: "test_value", secret: false },
|
||||||
|
{ key: "EXISTING_ENV_VAR_2", value: "", secret: false },
|
||||||
]),
|
]),
|
||||||
|
getCurrentEnvironment: () => ({
|
||||||
|
id: "1",
|
||||||
|
name: "some-env",
|
||||||
|
v: 1,
|
||||||
|
variables: {
|
||||||
|
key: "EXISTING_ENV_VAR",
|
||||||
|
value: "test_value",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getSelectedEnvironmentType: () => "MY_ENV",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,7 +63,7 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
|
|
||||||
expect(result.value).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment-not-found-0",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -91,7 +103,7 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
|
|
||||||
expect(result.value).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment-not-found-0",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -134,7 +146,7 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
|
|
||||||
expect(result.value).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment-not-found-0",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -161,5 +173,103 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
|
|
||||||
expect(result.value).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when the URL contains empty value in a environment variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "<<EXISTING_ENV_VAR_2>>",
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the URL contains non empty value in a environemnt variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "<<EXISTING_ENV_VAR>>",
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when the headers contain empty value in a environment variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR_2>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the headers contain non empty value in a environemnt variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when the params contain empty value in a environment variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [],
|
||||||
|
params: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR_2>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the params contain non empty value in a environemnt variable", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [],
|
||||||
|
params: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
|
expect(result.value).toHaveLength(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import { Service } from "dioc"
|
|||||||
import { Ref, markRaw } from "vue"
|
import { Ref, markRaw } from "vue"
|
||||||
import IconPlusCircle from "~icons/lucide/plus-circle"
|
import IconPlusCircle from "~icons/lucide/plus-circle"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { aggregateEnvs$ } from "~/newstore/environments"
|
import {
|
||||||
|
aggregateEnvsWithSecrets$,
|
||||||
|
getCurrentEnvironment,
|
||||||
|
getSelectedEnvironmentType,
|
||||||
|
} from "~/newstore/environments"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useStreamStatic } from "~/composables/stream"
|
import { useStreamStatic } from "~/composables/stream"
|
||||||
@@ -36,9 +40,13 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
|
|
||||||
private readonly inspection = this.bind(InspectionService)
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
private aggregateEnvs = useStreamStatic(aggregateEnvs$, [], () => {
|
private aggregateEnvsWithSecrets = useStreamStatic(
|
||||||
/* noop */
|
aggregateEnvsWithSecrets$,
|
||||||
})[0]
|
[],
|
||||||
|
() => {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
)[0]
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -49,9 +57,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
/**
|
/**
|
||||||
* Validates the environment variables in the target array
|
* Validates the environment variables in the target array
|
||||||
* @param target The target array to validate
|
* @param target The target array to validate
|
||||||
* @param results The results array to push the results to
|
|
||||||
* @param locations The location where results are to be displayed
|
* @param locations The location where results are to be displayed
|
||||||
* @returns The results array
|
* @returns The results array containing the results of the validation
|
||||||
*/
|
*/
|
||||||
private validateEnvironmentVariables = (
|
private validateEnvironmentVariables = (
|
||||||
target: any[],
|
target: any[],
|
||||||
@@ -59,7 +66,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
) => {
|
) => {
|
||||||
const newErrors: InspectorResult[] = []
|
const newErrors: InspectorResult[] = []
|
||||||
|
|
||||||
const envKeys = this.aggregateEnvs.value.map((e) => e.key)
|
const envKeys = this.aggregateEnvsWithSecrets.value.map((e) => e.key)
|
||||||
|
|
||||||
target.forEach((element, index) => {
|
target.forEach((element, index) => {
|
||||||
if (isENVInString(element)) {
|
if (isENVInString(element)) {
|
||||||
@@ -68,29 +75,20 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
if (extractedEnv) {
|
if (extractedEnv) {
|
||||||
extractedEnv.forEach((exEnv: string) => {
|
extractedEnv.forEach((exEnv: string) => {
|
||||||
const formattedExEnv = exEnv.slice(2, -2)
|
const formattedExEnv = exEnv.slice(2, -2)
|
||||||
let itemLocation: InspectorLocation
|
const itemLocation: InspectorLocation = {
|
||||||
if (locations.type === "header") {
|
type: locations.type,
|
||||||
itemLocation = {
|
position:
|
||||||
type: "header",
|
locations.type === "url" ||
|
||||||
position: locations.position,
|
locations.type === "body" ||
|
||||||
index: index,
|
locations.type === "response"
|
||||||
key: element,
|
? "key"
|
||||||
}
|
: locations.position,
|
||||||
} else if (locations.type === "parameter") {
|
index: index,
|
||||||
itemLocation = {
|
key: element,
|
||||||
type: "parameter",
|
|
||||||
position: locations.position,
|
|
||||||
index: index,
|
|
||||||
key: element,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemLocation = {
|
|
||||||
type: "url",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!envKeys.includes(formattedExEnv)) {
|
if (!envKeys.includes(formattedExEnv)) {
|
||||||
newErrors.push({
|
newErrors.push({
|
||||||
id: "environment",
|
id: `environment-not-found-${newErrors.length}`,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: this.t("inspections.environment.not_found", {
|
text: this.t("inspections.environment.not_found", {
|
||||||
@@ -112,7 +110,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
locations: itemLocation,
|
locations: itemLocation,
|
||||||
doc: {
|
doc: {
|
||||||
text: this.t("action.learn_more"),
|
text: this.t("action.learn_more"),
|
||||||
link: "https://docs.hoppscotch.io/",
|
link: "https://docs.hoppscotch.io/documentation/features/inspections",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -124,6 +122,103 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
return newErrors
|
return newErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the environment variables in the target array are empty
|
||||||
|
* @param target The target array to validate
|
||||||
|
* @param locations The location where results are to be displayed
|
||||||
|
* @returns The results array containing the results of the validation
|
||||||
|
*/
|
||||||
|
private validateEmptyEnvironmentVariables = (
|
||||||
|
target: any[],
|
||||||
|
locations: InspectorLocation
|
||||||
|
) => {
|
||||||
|
const newErrors: InspectorResult[] = []
|
||||||
|
|
||||||
|
target.forEach((element, index) => {
|
||||||
|
if (isENVInString(element)) {
|
||||||
|
const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
|
||||||
|
|
||||||
|
if (extractedEnv) {
|
||||||
|
extractedEnv.forEach((exEnv: string) => {
|
||||||
|
const formattedExEnv = exEnv.slice(2, -2)
|
||||||
|
|
||||||
|
this.aggregateEnvsWithSecrets.value.forEach((env) => {
|
||||||
|
if (env.key === formattedExEnv) {
|
||||||
|
if (env.value === "") {
|
||||||
|
const itemLocation: InspectorLocation = {
|
||||||
|
type: locations.type,
|
||||||
|
position:
|
||||||
|
locations.type === "url" ||
|
||||||
|
locations.type === "body" ||
|
||||||
|
locations.type === "response"
|
||||||
|
? "key"
|
||||||
|
: locations.position,
|
||||||
|
index: index,
|
||||||
|
key: element,
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSelectedEnvironment = getCurrentEnvironment()
|
||||||
|
const currentEnvironmentType = getSelectedEnvironmentType()
|
||||||
|
|
||||||
|
let invokeActionType:
|
||||||
|
| "modals.my.environment.edit"
|
||||||
|
| "modals.team.environment.edit"
|
||||||
|
| "modals.global.environment.update" =
|
||||||
|
"modals.my.environment.edit"
|
||||||
|
|
||||||
|
if (env.sourceEnv === "Global") {
|
||||||
|
invokeActionType = "modals.global.environment.update"
|
||||||
|
} else if (currentEnvironmentType === "MY_ENV") {
|
||||||
|
invokeActionType = "modals.my.environment.edit"
|
||||||
|
} else if (currentEnvironmentType === "TEAM_ENV") {
|
||||||
|
invokeActionType = "modals.team.environment.edit"
|
||||||
|
} else {
|
||||||
|
invokeActionType = "modals.my.environment.edit"
|
||||||
|
}
|
||||||
|
|
||||||
|
newErrors.push({
|
||||||
|
id: `environment-empty-${newErrors.length}`,
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("inspections.environment.empty_value", {
|
||||||
|
variable: exEnv,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
icon: markRaw(IconPlusCircle),
|
||||||
|
action: {
|
||||||
|
text: this.t(
|
||||||
|
"inspections.environment.add_environment_value"
|
||||||
|
),
|
||||||
|
apply: () => {
|
||||||
|
invokeAction(invokeActionType, {
|
||||||
|
envName:
|
||||||
|
env.sourceEnv !== "Global"
|
||||||
|
? currentSelectedEnvironment.name
|
||||||
|
: "Global",
|
||||||
|
variableName: formattedExEnv,
|
||||||
|
isSecret: env.secret,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: 2,
|
||||||
|
isApplicable: true,
|
||||||
|
locations: itemLocation,
|
||||||
|
doc: {
|
||||||
|
text: this.t("action.learn_more"),
|
||||||
|
link: "https://docs.hoppscotch.io/documentation/features/inspections",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return newErrors
|
||||||
|
}
|
||||||
|
|
||||||
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
|
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
const results: InspectorResult[] = []
|
const results: InspectorResult[] = []
|
||||||
@@ -132,12 +227,25 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
|
|
||||||
const params = req.value.params
|
const params = req.value.params
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the environment variables in the URL
|
||||||
|
*/
|
||||||
|
const url = req.value.endpoint
|
||||||
|
|
||||||
results.push(
|
results.push(
|
||||||
...this.validateEnvironmentVariables([req.value.endpoint], {
|
...this.validateEnvironmentVariables([url], {
|
||||||
|
type: "url",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
results.push(
|
||||||
|
...this.validateEmptyEnvironmentVariables([url], {
|
||||||
type: "url",
|
type: "url",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the environment variables in the headers
|
||||||
|
*/
|
||||||
const headerKeys = Object.values(headers).map((header) => header.key)
|
const headerKeys = Object.values(headers).map((header) => header.key)
|
||||||
|
|
||||||
results.push(
|
results.push(
|
||||||
@@ -146,6 +254,12 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
position: "key",
|
position: "key",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
results.push(
|
||||||
|
...this.validateEmptyEnvironmentVariables(headerKeys, {
|
||||||
|
type: "header",
|
||||||
|
position: "key",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const headerValues = Object.values(headers).map((header) => header.value)
|
const headerValues = Object.values(headers).map((header) => header.value)
|
||||||
|
|
||||||
@@ -155,7 +269,16 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
position: "value",
|
position: "value",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
results.push(
|
||||||
|
...this.validateEmptyEnvironmentVariables(headerValues, {
|
||||||
|
type: "header",
|
||||||
|
position: "value",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the environment variables in the parameters
|
||||||
|
*/
|
||||||
const paramsKeys = Object.values(params).map((param) => param.key)
|
const paramsKeys = Object.values(params).map((param) => param.key)
|
||||||
|
|
||||||
results.push(
|
results.push(
|
||||||
@@ -164,6 +287,12 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
position: "key",
|
position: "key",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
results.push(
|
||||||
|
...this.validateEmptyEnvironmentVariables(paramsKeys, {
|
||||||
|
type: "parameter",
|
||||||
|
position: "key",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const paramsValues = Object.values(params).map((param) => param.value)
|
const paramsValues = Object.values(params).map((param) => param.value)
|
||||||
|
|
||||||
@@ -174,6 +303,13 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
results.push(
|
||||||
|
...this.validateEmptyEnvironmentVariables(paramsValues, {
|
||||||
|
type: "parameter",
|
||||||
|
position: "value",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class HeaderInspectorService extends Service implements Inspector {
|
|||||||
},
|
},
|
||||||
doc: {
|
doc: {
|
||||||
text: this.t("action.learn_more"),
|
text: this.t("action.learn_more"),
|
||||||
link: "https://docs.hoppscotch.io/",
|
link: "https://docs.hoppscotch.io/documentation/features/inspections",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class ResponseInspectorService extends Service implements Inspector {
|
|||||||
},
|
},
|
||||||
doc: {
|
doc: {
|
||||||
text: this.t("action.learn_more"),
|
text: this.t("action.learn_more"),
|
||||||
link: "https://docs.hoppscotch.io/",
|
link: "https://docs.hoppscotch.io/documentation/features/inspections",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HoppGQLDocument } from "~/helpers/graphql/document"
|
|||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
||||||
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
|
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
|
||||||
|
import { SecretVariable } from "~/services/secret-environment.service"
|
||||||
import { PersistableTabState } from "~/services/tab"
|
import { PersistableTabState } from "~/services/tab"
|
||||||
|
|
||||||
type VUEX_DATA = {
|
type VUEX_DATA = {
|
||||||
@@ -19,7 +20,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
|||||||
|
|
||||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
{
|
{
|
||||||
v: 1,
|
v: 2,
|
||||||
name: "Echo",
|
name: "Echo",
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [
|
requests: [
|
||||||
@@ -36,12 +37,14 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||||||
body: { contentType: null, body: null },
|
body: { contentType: null, body: null },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
headers: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||||
{
|
{
|
||||||
v: 1,
|
v: 2,
|
||||||
name: "Echo",
|
name: "Echo",
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: [
|
requests: [
|
||||||
@@ -55,20 +58,30 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
auth: { authType: "none", authActive: true },
|
||||||
|
headers: [],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ENVIRONMENTS_MOCK: Environment[] = [
|
export const ENVIRONMENTS_MOCK: Environment[] = [
|
||||||
{
|
{
|
||||||
|
v: 1,
|
||||||
|
id: "ENV_1",
|
||||||
name: "globals",
|
name: "globals",
|
||||||
variables: [
|
variables: [
|
||||||
{
|
{
|
||||||
key: "test-global-key",
|
key: "test-global-key",
|
||||||
value: "test-global-value",
|
value: "test-global-value",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: "Test", variables: [{ key: "test-key", value: "test-value" }] },
|
{
|
||||||
|
v: 1,
|
||||||
|
id: "ENV_2",
|
||||||
|
name: "Test",
|
||||||
|
variables: [{ key: "test-key", value: "test-value", secret: false }],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SELECTED_ENV_INDEX_MOCK = {
|
export const SELECTED_ENV_INDEX_MOCK = {
|
||||||
@@ -98,7 +111,7 @@ export const MQTT_REQUEST_MOCK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GLOBAL_ENV_MOCK: Environment["variables"] = [
|
export const GLOBAL_ENV_MOCK: Environment["variables"] = [
|
||||||
{ key: "test-key", value: "test-value" },
|
{ key: "test-key", value: "test-value", secret: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const VUEX_DATA_MOCK: VUEX_DATA = {
|
export const VUEX_DATA_MOCK: VUEX_DATA = {
|
||||||
@@ -201,3 +214,11 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SECRET_ENVIRONMENTS_MOCK: Record<string, SecretVariable> = {
|
||||||
|
clryz7ir7002al4162bsj0azg: {
|
||||||
|
key: "test-key",
|
||||||
|
value: "test-value",
|
||||||
|
varIndex: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ import {
|
|||||||
REST_COLLECTIONS_MOCK,
|
REST_COLLECTIONS_MOCK,
|
||||||
REST_HISTORY_MOCK,
|
REST_HISTORY_MOCK,
|
||||||
REST_TAB_STATE_MOCK,
|
REST_TAB_STATE_MOCK,
|
||||||
|
SECRET_ENVIRONMENTS_MOCK,
|
||||||
SELECTED_ENV_INDEX_MOCK,
|
SELECTED_ENV_INDEX_MOCK,
|
||||||
SOCKET_IO_REQUEST_MOCK,
|
SOCKET_IO_REQUEST_MOCK,
|
||||||
SSE_REQUEST_MOCK,
|
SSE_REQUEST_MOCK,
|
||||||
VUEX_DATA_MOCK,
|
VUEX_DATA_MOCK,
|
||||||
WEBSOCKET_REQUEST_MOCK,
|
WEBSOCKET_REQUEST_MOCK,
|
||||||
} from "./__mocks__"
|
} from "./__mocks__"
|
||||||
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
|
||||||
vi.mock("~/modules/i18n", () => {
|
vi.mock("~/modules/i18n", () => {
|
||||||
return {
|
return {
|
||||||
@@ -122,10 +124,12 @@ const spyOnSetItem = () => vi.spyOn(Storage.prototype, "setItem")
|
|||||||
const bindPersistenceService = ({
|
const bindPersistenceService = ({
|
||||||
mockGQLTabService = false,
|
mockGQLTabService = false,
|
||||||
mockRESTTabService = false,
|
mockRESTTabService = false,
|
||||||
|
mockSecretEnvironmentsService = false,
|
||||||
mock = {},
|
mock = {},
|
||||||
}: {
|
}: {
|
||||||
mockGQLTabService?: boolean
|
mockGQLTabService?: boolean
|
||||||
mockRESTTabService?: boolean
|
mockRESTTabService?: boolean
|
||||||
|
mockSecretEnvironmentsService?: boolean
|
||||||
mock?: Record<string, unknown>
|
mock?: Record<string, unknown>
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
@@ -138,6 +142,10 @@ const bindPersistenceService = ({
|
|||||||
container.bindMock(RESTTabService, mock)
|
container.bindMock(RESTTabService, mock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mockSecretEnvironmentsService) {
|
||||||
|
container.bindMock(SecretEnvironmentService, mock)
|
||||||
|
}
|
||||||
|
|
||||||
container.bind(PersistenceService)
|
container.bind(PersistenceService)
|
||||||
|
|
||||||
const service = container.bind(PersistenceService)
|
const service = container.bind(PersistenceService)
|
||||||
@@ -893,7 +901,12 @@ describe("PersistenceService", () => {
|
|||||||
// Invalid shape for `environments`
|
// Invalid shape for `environments`
|
||||||
const environments = [
|
const environments = [
|
||||||
// `entries` -> `variables`
|
// `entries` -> `variables`
|
||||||
{ name: "Test", entries: [{ key: "test-key", value: "test-value" }] },
|
{
|
||||||
|
v: 1,
|
||||||
|
id: "ENV_1",
|
||||||
|
name: "Test",
|
||||||
|
entries: [{ key: "test-key", value: "test-value", secret: false }],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@@ -1044,6 +1057,119 @@ describe("PersistenceService", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Setup secret Environments persistence", () => {
|
||||||
|
// Key read from localStorage across test cases
|
||||||
|
const secretEnvironmentsKey = "secretEnvironments"
|
||||||
|
|
||||||
|
const loadSecretEnvironmentsFromPersistedStateFn = vi.fn()
|
||||||
|
const mock = {
|
||||||
|
loadSecretEnvironmentsFromPersistedState:
|
||||||
|
loadSecretEnvironmentsFromPersistedStateFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
it(`shows an error and sets the entry as a backup in localStorage if "${secretEnvironmentsKey}" read from localStorage doesn't match the schema`, () => {
|
||||||
|
// Invalid shape for `secretEnvironments`
|
||||||
|
const secretEnvironments = {
|
||||||
|
clryz7ir7002al4162bsj0azg: {
|
||||||
|
key: "ENV_KEY",
|
||||||
|
value: "ENV_VALUE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(
|
||||||
|
secretEnvironmentsKey,
|
||||||
|
JSON.stringify(secretEnvironments)
|
||||||
|
)
|
||||||
|
|
||||||
|
const getItemSpy = spyOnGetItem()
|
||||||
|
const setItemSpy = spyOnSetItem()
|
||||||
|
|
||||||
|
invokeSetupLocalPersistence()
|
||||||
|
|
||||||
|
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
expect(toastErrorFn).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(secretEnvironmentsKey)
|
||||||
|
)
|
||||||
|
expect(setItemSpy).toHaveBeenCalledWith(
|
||||||
|
`${secretEnvironmentsKey}-backup`,
|
||||||
|
JSON.stringify(secretEnvironments)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("loads secret environments from the state persisted in localStorage and sets watcher for `persistableSecretEnvironment`", () => {
|
||||||
|
const secretEnvironment = SECRET_ENVIRONMENTS_MOCK
|
||||||
|
window.localStorage.setItem(
|
||||||
|
secretEnvironmentsKey,
|
||||||
|
JSON.stringify(secretEnvironment)
|
||||||
|
)
|
||||||
|
|
||||||
|
const getItemSpy = spyOnGetItem()
|
||||||
|
|
||||||
|
invokeSetupLocalPersistence({
|
||||||
|
mockSecretEnvironmentsService: true,
|
||||||
|
mock,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
expect(loadSecretEnvironmentsFromPersistedStateFn).toHaveBeenCalledWith(
|
||||||
|
secretEnvironment
|
||||||
|
)
|
||||||
|
expect(watchDebounced).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expect.any(Function),
|
||||||
|
{ debounce: 500 }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`skips schema parsing and the loading of persisted secret environments if there is no "${secretEnvironmentsKey}" key present in localStorage`, () => {
|
||||||
|
window.localStorage.removeItem(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
const getItemSpy = spyOnGetItem()
|
||||||
|
const setItemSpy = spyOnSetItem()
|
||||||
|
|
||||||
|
invokeSetupLocalPersistence({
|
||||||
|
mockSecretEnvironmentsService: true,
|
||||||
|
mock,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
expect(setItemSpy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(watchDebounced).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("logs an error to the console on failing to parse persisted secret environments", () => {
|
||||||
|
window.localStorage.setItem(secretEnvironmentsKey, "invalid-json")
|
||||||
|
|
||||||
|
console.error = vi.fn()
|
||||||
|
const getItemSpy = spyOnGetItem()
|
||||||
|
const setItemSpy = spyOnSetItem()
|
||||||
|
|
||||||
|
invokeSetupLocalPersistence()
|
||||||
|
|
||||||
|
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
|
||||||
|
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
|
||||||
|
expect(setItemSpy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
|
`Failed parsing persisted secret environment, state:`,
|
||||||
|
window.localStorage.getItem(secretEnvironmentsKey)
|
||||||
|
)
|
||||||
|
expect(watchDebounced).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expect.any(Function),
|
||||||
|
{ debounce: 500 }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("setup WebSocket persistence", () => {
|
describe("setup WebSocket persistence", () => {
|
||||||
// Key read from localStorage across test cases
|
// Key read from localStorage across test cases
|
||||||
const wsRequestKey = "WebsocketRequest"
|
const wsRequestKey = "WebsocketRequest"
|
||||||
|
|||||||
@@ -67,10 +67,12 @@ import {
|
|||||||
SETTINGS_SCHEMA,
|
SETTINGS_SCHEMA,
|
||||||
SOCKET_IO_REQUEST_SCHEMA,
|
SOCKET_IO_REQUEST_SCHEMA,
|
||||||
SSE_REQUEST_SCHEMA,
|
SSE_REQUEST_SCHEMA,
|
||||||
|
SECRET_ENVIRONMENT_VARIABLE_SCHEMA,
|
||||||
THEME_COLOR_SCHEMA,
|
THEME_COLOR_SCHEMA,
|
||||||
VUEX_SCHEMA,
|
VUEX_SCHEMA,
|
||||||
WEBSOCKET_REQUEST_SCHEMA,
|
WEBSOCKET_REQUEST_SCHEMA,
|
||||||
} from "./validation-schemas"
|
} from "./validation-schemas"
|
||||||
|
import { SecretEnvironmentService } from "../secret-environment.service"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service compiles persistence logic across the codebase
|
* This service compiles persistence logic across the codebase
|
||||||
@@ -81,6 +83,10 @@ export class PersistenceService extends Service {
|
|||||||
private readonly restTabService = this.bind(RESTTabService)
|
private readonly restTabService = this.bind(RESTTabService)
|
||||||
private readonly gqlTabService = this.bind(GQLTabService)
|
private readonly gqlTabService = this.bind(GQLTabService)
|
||||||
|
|
||||||
|
private readonly secretEnvironmentService = this.bind(
|
||||||
|
SecretEnvironmentService
|
||||||
|
)
|
||||||
|
|
||||||
public hoppLocalConfigStorage: StorageLike = localStorage
|
public hoppLocalConfigStorage: StorageLike = localStorage
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -438,6 +444,56 @@ export class PersistenceService extends Service {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupSecretEnvironmentsPersistence() {
|
||||||
|
const secretEnvironmentsKey = "secretEnvironments"
|
||||||
|
const secretEnvironmentsData = window.localStorage.getItem(
|
||||||
|
secretEnvironmentsKey
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (secretEnvironmentsData) {
|
||||||
|
let parsedSecretEnvironmentsData = JSON.parse(secretEnvironmentsData)
|
||||||
|
|
||||||
|
// Validate data read from localStorage
|
||||||
|
const result = SECRET_ENVIRONMENT_VARIABLE_SCHEMA.safeParse(
|
||||||
|
parsedSecretEnvironmentsData
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
parsedSecretEnvironmentsData = result.data
|
||||||
|
} else {
|
||||||
|
this.showErrorToast(secretEnvironmentsKey)
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${secretEnvironmentsKey}-backup`,
|
||||||
|
JSON.stringify(parsedSecretEnvironmentsData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.secretEnvironmentService.loadSecretEnvironmentsFromPersistedState(
|
||||||
|
parsedSecretEnvironmentsData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Failed parsing persisted secret environment, state:`,
|
||||||
|
secretEnvironmentsData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
this.secretEnvironmentService.persistableSecretEnvironments,
|
||||||
|
(newSecretEnvironment) => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
secretEnvironmentsKey,
|
||||||
|
JSON.stringify(newSecretEnvironment)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
debounce: 500,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private setupSelectedEnvPersistence() {
|
private setupSelectedEnvPersistence() {
|
||||||
const selectedEnvIndexKey = "selectedEnvIndex"
|
const selectedEnvIndexKey = "selectedEnvIndex"
|
||||||
let selectedEnvIndexValue = JSON.parse(
|
let selectedEnvIndexValue = JSON.parse(
|
||||||
@@ -697,6 +753,8 @@ export class PersistenceService extends Service {
|
|||||||
this.setupSocketIOPersistence()
|
this.setupSocketIOPersistence()
|
||||||
this.setupSSEPersistence()
|
this.setupSSEPersistence()
|
||||||
this.setupMQTTPersistence()
|
this.setupMQTTPersistence()
|
||||||
|
|
||||||
|
this.setupSecretEnvironmentsPersistence()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ export const SELECTED_ENV_INDEX_SCHEMA = z.nullable(
|
|||||||
type: z.literal("TEAM_ENV"),
|
type: z.literal("TEAM_ENV"),
|
||||||
teamID: z.string(),
|
teamID: z.string(),
|
||||||
teamEnvID: z.string(),
|
teamEnvID: z.string(),
|
||||||
// ! Versioned entity
|
|
||||||
environment: entityReference(Environment),
|
environment: entityReference(Environment),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@@ -212,13 +211,19 @@ export const MQTT_REQUEST_SCHEMA = z.nullable(
|
|||||||
|
|
||||||
export const GLOBAL_ENV_SCHEMA = z.union([
|
export const GLOBAL_ENV_SCHEMA = z.union([
|
||||||
z.array(z.never()),
|
z.array(z.never()),
|
||||||
|
|
||||||
z.array(
|
z.array(
|
||||||
z
|
z.union([
|
||||||
.object({
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
secret: z.literal(true),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
})
|
secret: z.literal(false),
|
||||||
.strict()
|
}),
|
||||||
|
])
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -339,12 +344,34 @@ const HoppTestDataSchema = z.lazy(() =>
|
|||||||
.strict()
|
.strict()
|
||||||
)
|
)
|
||||||
|
|
||||||
const EnvironmentVariablesSchema = z
|
const EnvironmentVariablesSchema = z.union([
|
||||||
.object({
|
z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
})
|
secret: z.literal(false),
|
||||||
.strict()
|
}),
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
secret: z.literal(true),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const SECRET_ENVIRONMENT_VARIABLE_SCHEMA = z.union([
|
||||||
|
z.object({}).strict(),
|
||||||
|
|
||||||
|
z.record(
|
||||||
|
z.string(),
|
||||||
|
z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
varIndex: z.number(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
const HoppTestResultSchema = z
|
const HoppTestResultSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -358,7 +385,11 @@ const HoppTestResultSchema = z
|
|||||||
.object({
|
.object({
|
||||||
additions: z.array(EnvironmentVariablesSchema),
|
additions: z.array(EnvironmentVariablesSchema),
|
||||||
updations: z.array(
|
updations: z.array(
|
||||||
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
EnvironmentVariablesSchema.refine((x) => !x.secret).and(
|
||||||
|
z.object({
|
||||||
|
previousValue: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
),
|
),
|
||||||
deletions: z.array(EnvironmentVariablesSchema),
|
deletions: z.array(EnvironmentVariablesSchema),
|
||||||
})
|
})
|
||||||
@@ -367,7 +398,11 @@ const HoppTestResultSchema = z
|
|||||||
.object({
|
.object({
|
||||||
additions: z.array(EnvironmentVariablesSchema),
|
additions: z.array(EnvironmentVariablesSchema),
|
||||||
updations: z.array(
|
updations: z.array(
|
||||||
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
EnvironmentVariablesSchema.refine((x) => !x.secret).and(
|
||||||
|
z.object({
|
||||||
|
previousValue: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
),
|
),
|
||||||
deletions: z.array(EnvironmentVariablesSchema),
|
deletions: z.array(EnvironmentVariablesSchema),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import { reactive } from "vue"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a secret environment variable.
|
||||||
|
*/
|
||||||
|
export type SecretVariable = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
varIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service is used to store and manage secret environments.
|
||||||
|
* The secret environments are not synced with the server.
|
||||||
|
* hence they are not persisted in the database. They are stored
|
||||||
|
* in the local storage of the browser.
|
||||||
|
*/
|
||||||
|
export class SecretEnvironmentService extends Service {
|
||||||
|
public static readonly ID = "SECRET_ENVIRONMENT_SERVICE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of secret environments.
|
||||||
|
* The key is the ID of the secret environment.
|
||||||
|
* The value is the list of secret variables.
|
||||||
|
*/
|
||||||
|
public secretEnvironments = reactive(new Map<string, SecretVariable[]>())
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new secret environment.
|
||||||
|
* @param id ID of the environment
|
||||||
|
* @param secretVars List of secret variables
|
||||||
|
*/
|
||||||
|
public addSecretEnvironment(id: string, secretVars: SecretVariable[]) {
|
||||||
|
this.secretEnvironments.set(id, secretVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a secret environment.
|
||||||
|
* @param id ID of the environment
|
||||||
|
*/
|
||||||
|
public getSecretEnvironment(id: string) {
|
||||||
|
return this.secretEnvironments.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a secret environment variable.
|
||||||
|
* @param id ID of the environment
|
||||||
|
* @param varIndex Index of the variable in the environment
|
||||||
|
*/
|
||||||
|
public getSecretEnvironmentVariable(id: string, varIndex: number) {
|
||||||
|
const secretVars = this.getSecretEnvironment(id)
|
||||||
|
return secretVars?.find((secretVar) => secretVar.varIndex === varIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to get the value of a secret environment variable.
|
||||||
|
* @param id ID of the environment
|
||||||
|
* @param varIndex Index of the variable in the environment
|
||||||
|
= */
|
||||||
|
public getSecretEnvironmentVariableValue(id: string, varIndex: number) {
|
||||||
|
const secretVar = this.getSecretEnvironmentVariable(id, varIndex)
|
||||||
|
return secretVar?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param secretEnvironments Used to load secret environments from persisted state.
|
||||||
|
*/
|
||||||
|
public loadSecretEnvironmentsFromPersistedState(
|
||||||
|
secretEnvironments: Record<string, SecretVariable[]>
|
||||||
|
) {
|
||||||
|
if (secretEnvironments) {
|
||||||
|
this.secretEnvironments.clear()
|
||||||
|
|
||||||
|
Object.entries(secretEnvironments).forEach(([id, secretVars]) => {
|
||||||
|
this.addSecretEnvironment(id, secretVars)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a secret environment.
|
||||||
|
* @param id ID of the environment
|
||||||
|
*/
|
||||||
|
public deleteSecretEnvironment(id: string) {
|
||||||
|
this.secretEnvironments.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a secret environment variable.
|
||||||
|
* @param id ID of the environment
|
||||||
|
* @param varIndex Index of the variable in the environment
|
||||||
|
*/
|
||||||
|
public removeSecretEnvironmentVariable(id: string, varIndex: number) {
|
||||||
|
const secretVars = this.getSecretEnvironment(id)
|
||||||
|
const newSecretVars = secretVars?.filter(
|
||||||
|
(secretVar) => secretVar.varIndex !== varIndex
|
||||||
|
)
|
||||||
|
this.secretEnvironments.set(id, newSecretVars || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update thye ID of a secret environment.
|
||||||
|
* Used while syncing with the server.
|
||||||
|
* @param oldID old ID of the environment
|
||||||
|
* @param newID new ID of the environment
|
||||||
|
*/
|
||||||
|
public updateSecretEnvironmentID(oldID: string, newID: string) {
|
||||||
|
const secretVars = this.getSecretEnvironment(oldID)
|
||||||
|
this.secretEnvironments.set(newID, secretVars || [])
|
||||||
|
this.secretEnvironments.delete(oldID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update the value of a secret environment variable.
|
||||||
|
*/
|
||||||
|
public persistableSecretEnvironments = computed(() => {
|
||||||
|
const secretEnvironments: Record<string, SecretVariable[]> = {}
|
||||||
|
this.secretEnvironments.forEach((secretVars, id) => {
|
||||||
|
secretEnvironments[id] = secretVars
|
||||||
|
})
|
||||||
|
return secretEnvironments
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -248,9 +248,7 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
|
|||||||
this.duplicateSelectedEnv()
|
this.duplicateSelectedEnv()
|
||||||
break
|
break
|
||||||
case "edit_global_env":
|
case "edit_global_env":
|
||||||
invokeAction(`modals.my.environment.edit`, {
|
invokeAction(`modals.global.environment.update`, {})
|
||||||
envName: "Global",
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case "duplicate_global_env":
|
case "duplicate_global_env":
|
||||||
this.duplicateGlobalEnv()
|
this.duplicateGlobalEnv()
|
||||||
|
|||||||
@@ -2,22 +2,38 @@ import * as E from "fp-ts/Either"
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import { InferredEntity, createVersionedEntity } from "verzod"
|
import { InferredEntity, createVersionedEntity } from "verzod"
|
||||||
|
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import V0_VERSION from "./v/0"
|
import V0_VERSION from "./v/0"
|
||||||
|
import V1_VERSION from "./v/1"
|
||||||
|
|
||||||
|
const versionedObject = z.object({
|
||||||
|
v: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
export const Environment = createVersionedEntity({
|
export const Environment = createVersionedEntity({
|
||||||
latestVersion: 0,
|
latestVersion: 1,
|
||||||
versionMap: {
|
versionMap: {
|
||||||
0: V0_VERSION
|
0: V0_VERSION,
|
||||||
|
1: V1_VERSION,
|
||||||
|
},
|
||||||
|
getVersion(data) {
|
||||||
|
const versionCheck = versionedObject.safeParse(data)
|
||||||
|
|
||||||
|
if (versionCheck.success) return versionCheck.data.v
|
||||||
|
|
||||||
|
// For V0 we have to check the schema
|
||||||
|
const result = V0_VERSION.schema.safeParse(data)
|
||||||
|
return result.success ? 0 : null
|
||||||
},
|
},
|
||||||
getVersion(x) {
|
|
||||||
return V0_VERSION.schema.safeParse(x).success
|
|
||||||
? 0
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Environment = InferredEntity<typeof Environment>
|
export type Environment = InferredEntity<typeof Environment>
|
||||||
|
|
||||||
|
export type EnvironmentVariable = InferredEntity<
|
||||||
|
typeof Environment
|
||||||
|
>["variables"][number]
|
||||||
|
|
||||||
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
|
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +47,8 @@ const ENV_MAX_EXPAND_LIMIT = 10
|
|||||||
*/
|
*/
|
||||||
const ENV_EXPAND_LOOP = "ENV_EXPAND_LOOP" as const
|
const ENV_EXPAND_LOOP = "ENV_EXPAND_LOOP" as const
|
||||||
|
|
||||||
|
export const EnvironmentSchemaVersion = 1
|
||||||
|
|
||||||
export function parseBodyEnvVariablesE(
|
export function parseBodyEnvVariablesE(
|
||||||
body: string,
|
body: string,
|
||||||
env: Environment["variables"]
|
env: Environment["variables"]
|
||||||
@@ -43,7 +61,11 @@ export function parseBodyEnvVariablesE(
|
|||||||
const found = env.find(
|
const found = env.find(
|
||||||
(envVar) => envVar.key === key.replace(/[<>]/g, "")
|
(envVar) => envVar.key === key.replace(/[<>]/g, "")
|
||||||
)
|
)
|
||||||
return found ? found.value : key
|
|
||||||
|
if (found && "value" in found) {
|
||||||
|
return found.value
|
||||||
|
}
|
||||||
|
return key
|
||||||
})
|
})
|
||||||
|
|
||||||
depth++
|
depth++
|
||||||
@@ -68,7 +90,10 @@ export const parseBodyEnvVariables = (
|
|||||||
|
|
||||||
export function parseTemplateStringE(
|
export function parseTemplateStringE(
|
||||||
str: string,
|
str: string,
|
||||||
variables: Environment["variables"]
|
variables:
|
||||||
|
| Environment["variables"]
|
||||||
|
| { secret: true; value: string; key: string }[],
|
||||||
|
maskValue = false
|
||||||
) {
|
) {
|
||||||
if (!variables || !str) {
|
if (!variables || !str) {
|
||||||
return E.right(str)
|
return E.right(str)
|
||||||
@@ -78,10 +103,21 @@ export function parseTemplateStringE(
|
|||||||
let depth = 0
|
let depth = 0
|
||||||
|
|
||||||
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
|
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
|
||||||
result = decodeURI(encodeURI(result)).replace(
|
result = decodeURI(encodeURI(result)).replace(REGEX_ENV_VAR, (_, p1) => {
|
||||||
REGEX_ENV_VAR,
|
const variable = variables.find((x) => x && x.key === p1)
|
||||||
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
|
if (variable && "value" in variable) {
|
||||||
)
|
// Mask the value if it is a secret and explicitly specified
|
||||||
|
if (variable.secret && maskValue) {
|
||||||
|
return "*".repeat(
|
||||||
|
(variable as { secret: true; value: string; key: string }).value
|
||||||
|
.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return variable.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
depth++
|
depth++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +126,52 @@ export function parseTemplateStringE(
|
|||||||
: E.right(result)
|
: E.right(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NonSecretEnvironmentVariable = Extract<
|
||||||
|
EnvironmentVariable,
|
||||||
|
{ secret: false }
|
||||||
|
>
|
||||||
|
|
||||||
|
export type NonSecretEnvironment = Omit<Environment, "variables"> & {
|
||||||
|
variables: NonSecretEnvironmentVariable[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `parseTemplateStringE` instead
|
* @deprecated Use `parseTemplateStringE` instead
|
||||||
*/
|
*/
|
||||||
export const parseTemplateString = (
|
export const parseTemplateString = (
|
||||||
str: string,
|
str: string,
|
||||||
variables: Environment["variables"]
|
variables:
|
||||||
|
| Environment["variables"]
|
||||||
|
| { secret: true; value: string; key: string }[],
|
||||||
|
maskValue = false
|
||||||
) =>
|
) =>
|
||||||
pipe(
|
pipe(
|
||||||
parseTemplateStringE(str, variables),
|
parseTemplateStringE(str, variables, maskValue),
|
||||||
E.getOrElse(() => str)
|
E.getOrElse(() => str)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const translateToNewEnvironmentVariables = (
|
||||||
|
x: any
|
||||||
|
): Environment["variables"][number] => {
|
||||||
|
return {
|
||||||
|
key: x.key,
|
||||||
|
value: x.value,
|
||||||
|
secret: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translateToNewEnvironment = (x: any): Environment => {
|
||||||
|
if (x.v && x.v === EnvironmentSchemaVersion) return x
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
const id = x.id ?? ""
|
||||||
|
const name = x.name ?? "Untitled"
|
||||||
|
const variables = (x.variables ?? []).map(translateToNewEnvironmentVariables)
|
||||||
|
|
||||||
|
return {
|
||||||
|
v: EnvironmentSchemaVersion,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
variables,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export const V0_SCHEMA = z.object({
|
|||||||
key: z.string(),
|
key: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default defineVersion({
|
export default defineVersion({
|
||||||
initial: true,
|
initial: true,
|
||||||
schema: V0_SCHEMA
|
schema: V0_SCHEMA,
|
||||||
})
|
})
|
||||||
|
|||||||
42
packages/hoppscotch-data/src/environment/v/1.ts
Normal file
42
packages/hoppscotch-data/src/environment/v/1.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { defineVersion } from "verzod"
|
||||||
|
import { V0_SCHEMA } from "./0"
|
||||||
|
|
||||||
|
export const V1_SCHEMA = z.object({
|
||||||
|
v: z.literal(1),
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
variables: z.array(
|
||||||
|
z.union([
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
secret: z.literal(true),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
secret: z.literal(false),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineVersion({
|
||||||
|
initial: false,
|
||||||
|
schema: V1_SCHEMA,
|
||||||
|
up(old: z.infer<typeof V0_SCHEMA>) {
|
||||||
|
const result: z.infer<typeof V1_SCHEMA> = {
|
||||||
|
...old,
|
||||||
|
v: 1,
|
||||||
|
id: old.id ?? "",
|
||||||
|
variables: old.variables.map((variable) => {
|
||||||
|
return {
|
||||||
|
...variable,
|
||||||
|
secret: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -12,16 +12,16 @@ describe("runPreRequestScript", () => {
|
|||||||
{
|
{
|
||||||
global: [],
|
global: [],
|
||||||
selected: [
|
selected: [
|
||||||
{ key: "bob", value: "oldbob" },
|
{ key: "bob", value: "oldbob", secret: false },
|
||||||
{ key: "foo", value: "bar" },
|
{ key: "foo", value: "bar", secret: false },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)()
|
)()
|
||||||
).resolves.toEqualRight({
|
).resolves.toEqualRight({
|
||||||
global: [],
|
global: [],
|
||||||
selected: [
|
selected: [
|
||||||
{ key: "bob", value: "newbob" },
|
{ key: "bob", value: "newbob", secret: false },
|
||||||
{ key: "foo", value: "bar" },
|
{ key: "foo", value: "bar", secret: false },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -35,8 +35,8 @@ describe("runPreRequestScript", () => {
|
|||||||
{
|
{
|
||||||
global: [],
|
global: [],
|
||||||
selected: [
|
selected: [
|
||||||
{ key: "bob", value: "oldbob" },
|
{ key: "bob", value: "oldbob", secret: false },
|
||||||
{ key: "foo", value: "bar" },
|
{ key: "foo", value: "bar", secret: false },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)()
|
)()
|
||||||
@@ -52,8 +52,8 @@ describe("runPreRequestScript", () => {
|
|||||||
{
|
{
|
||||||
global: [],
|
global: [],
|
||||||
selected: [
|
selected: [
|
||||||
{ key: "bob", value: "oldbob" },
|
{ key: "bob", value: "oldbob", secret: false },
|
||||||
{ key: "foo", value: "bar" },
|
{ key: "foo", value: "bar", secret: false },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)()
|
)()
|
||||||
@@ -69,8 +69,8 @@ describe("runPreRequestScript", () => {
|
|||||||
{
|
{
|
||||||
global: [],
|
global: [],
|
||||||
selected: [
|
selected: [
|
||||||
{ key: "bob", value: "oldbob" },
|
{ key: "bob", value: "oldbob", secret: false },
|
||||||
{ key: "foo", value: "bar" },
|
{ key: "foo", value: "bar", secret: false },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)()
|
)()
|
||||||
@@ -87,7 +87,7 @@ describe("runPreRequestScript", () => {
|
|||||||
)()
|
)()
|
||||||
).resolves.toEqualRight({
|
).resolves.toEqualRight({
|
||||||
global: [],
|
global: [],
|
||||||
selected: [{ key: "foo", value: "bar" }],
|
selected: [{ key: "foo", value: "bar", secret: false }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ describe("Base64 helper functions", () => {
|
|||||||
atob: {
|
atob: {
|
||||||
script: `pw.env.set("atob", atob("SGVsbG8gV29ybGQ="))`,
|
script: `pw.env.set("atob", atob("SGVsbG8gV29ybGQ="))`,
|
||||||
environment: {
|
environment: {
|
||||||
selected: [{ key: "atob", value: "Hello World" }],
|
selected: [{ key: "atob", value: "Hello World", secret: false }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
btoa: {
|
btoa: {
|
||||||
script: `pw.env.set("btoa", btoa("Hello World"))`,
|
script: `pw.env.set("btoa", btoa("Hello World"))`,
|
||||||
environment: {
|
environment: {
|
||||||
selected: [{ key: "btoa", value: "SGVsbG8gV29ybGQ=" }],
|
selected: [{ key: "btoa", value: "SGVsbG8gV29ybGQ=", secret: false }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("pw.env.get", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ describe("pw.env.get", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -112,12 +114,14 @@ describe("pw.env.get", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "global val",
|
value: "global val",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "selected val",
|
value: "selected val",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -147,6 +151,7 @@ describe("pw.env.get", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "<<hello>>",
|
value: "<<hello>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("pw.env.getResolve", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ describe("pw.env.getResolve", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -112,12 +114,14 @@ describe("pw.env.getResolve", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "global val",
|
value: "global val",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "selected val",
|
value: "selected val",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -147,10 +151,12 @@ describe("pw.env.getResolve", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "<<hello>>",
|
value: "<<hello>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "there",
|
value: "there",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -180,10 +186,12 @@ describe("pw.env.getResolve", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "<<hello>>",
|
value: "<<hello>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "<<a>>",
|
value: "<<a>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ describe("pw.env.resolve", () => {
|
|||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "there",
|
value: "there",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -73,6 +74,7 @@ describe("pw.env.resolve", () => {
|
|||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "there",
|
value: "there",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -101,12 +103,14 @@ describe("pw.env.resolve", () => {
|
|||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "yo",
|
value: "yo",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "there",
|
value: "there",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -136,10 +140,12 @@ describe("pw.env.resolve", () => {
|
|||||||
{
|
{
|
||||||
key: "hello",
|
key: "hello",
|
||||||
value: "<<there>>",
|
value: "<<there>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "there",
|
key: "there",
|
||||||
value: "<<hello>>",
|
value: "<<hello>>",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,7 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "c",
|
value: "c",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -62,6 +64,7 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -73,6 +76,7 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "c",
|
value: "c",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -90,12 +94,14 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "d",
|
value: "d",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -106,12 +112,14 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "b",
|
value: "b",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "c",
|
value: "c",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -136,6 +144,7 @@ describe("pw.env.set", () => {
|
|||||||
{
|
{
|
||||||
key: "a",
|
key: "a",
|
||||||
value: "c",
|
value: "c",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,7 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -80,12 +82,14 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://httpbin.org",
|
value: "https://httpbin.org",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,7 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://httpbin.org",
|
value: "https://httpbin.org",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -114,16 +119,19 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://httpbin.org",
|
value: "https://httpbin.org",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -134,12 +142,14 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [
|
selected: [
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -157,10 +167,12 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://httpbin.org/",
|
value: "https://httpbin.org/",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -172,6 +184,7 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -225,6 +238,7 @@ describe("pw.env.unset", () => {
|
|||||||
{
|
{
|
||||||
key: "baseUrl",
|
key: "baseUrl",
|
||||||
value: "https://echo.hoppscotch.io",
|
value: "https://echo.hoppscotch.io",
|
||||||
|
secret: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Environment } from "@hoppscotch/data"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response object structure exposed to the test script
|
* The response object structure exposed to the test script
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +41,13 @@ export type TestDescriptor = {
|
|||||||
children: TestDescriptor[]
|
children: TestDescriptor[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Representation of a transformed state for environment variables in the sandbox
|
||||||
|
type TransformedEnvironmentVariable = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
secret: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the result of a test script execution
|
* Defines the result of a test script execution
|
||||||
*/
|
*/
|
||||||
@@ -50,8 +55,8 @@ export type TestDescriptor = {
|
|||||||
export type TestResult = {
|
export type TestResult = {
|
||||||
tests: TestDescriptor[]
|
tests: TestDescriptor[]
|
||||||
envs: {
|
envs: {
|
||||||
global: Environment["variables"]
|
global: TransformedEnvironmentVariable[]
|
||||||
selected: Environment["variables"]
|
selected: TransformedEnvironmentVariable[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,20 @@ const setEnv = (
|
|||||||
const indexInGlobal = findEnvIndex(envName, global)
|
const indexInGlobal = findEnvIndex(envName, global)
|
||||||
|
|
||||||
if (indexInSelected >= 0) {
|
if (indexInSelected >= 0) {
|
||||||
selected[indexInSelected].value = envValue
|
const selectedEnv = selected[indexInSelected]
|
||||||
|
if ("value" in selectedEnv) {
|
||||||
|
selectedEnv.value = envValue
|
||||||
|
}
|
||||||
} else if (indexInGlobal >= 0) {
|
} else if (indexInGlobal >= 0) {
|
||||||
global[indexInGlobal].value = envValue
|
if ("value" in global[indexInGlobal]) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extra-semi
|
||||||
|
;(global[indexInGlobal] as { value: string }).value = envValue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selected.push({
|
selected.push({
|
||||||
key: envName,
|
key: envName,
|
||||||
value: envValue,
|
value: envValue,
|
||||||
|
secret: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +93,9 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
|
|||||||
|
|
||||||
const result = pipe(
|
const result = pipe(
|
||||||
getEnv(key, updatedEnvs),
|
getEnv(key, updatedEnvs),
|
||||||
O.match(
|
O.fold(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
({ value }) => String(value)
|
(env) => String(env.value)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,14 +111,13 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
|
|||||||
getEnv(key, updatedEnvs),
|
getEnv(key, updatedEnvs),
|
||||||
E.fromOption(() => "INVALID_KEY" as const),
|
E.fromOption(() => "INVALID_KEY" as const),
|
||||||
|
|
||||||
E.map(({ value }) =>
|
E.map((e) =>
|
||||||
pipe(
|
pipe(
|
||||||
parseTemplateStringE(value, [
|
parseTemplateStringE(e.value, [
|
||||||
...updatedEnvs.selected,
|
...updatedEnvs.selected,
|
||||||
...updatedEnvs.global,
|
...updatedEnvs.global,
|
||||||
]),
|
]), // If the recursive resolution failed, return the unresolved value
|
||||||
// If the recursive resolution failed, return the unresolved value
|
E.getOrElse(() => e.value)
|
||||||
E.getOrElse(() => value)
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
E.map((x) => String(x)),
|
E.map((x) => String(x)),
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ async function loadUserEnvironments() {
|
|||||||
runDispatchWithOutSyncing(() => {
|
runDispatchWithOutSyncing(() => {
|
||||||
replaceEnvironments(
|
replaceEnvironments(
|
||||||
environments.map(({ id, variables, name }) => ({
|
environments.map(({ id, variables, name }) => ({
|
||||||
|
v: 1,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
variables: JSON.parse(variables),
|
variables: JSON.parse(variables),
|
||||||
@@ -164,6 +165,7 @@ function setupUserEnvironmentUpdatedSubscription() {
|
|||||||
if ((localIndex || localIndex == 0) && name) {
|
if ((localIndex || localIndex == 0) && name) {
|
||||||
runDispatchWithOutSyncing(() => {
|
runDispatchWithOutSyncing(() => {
|
||||||
updateEnvironment(localIndex, {
|
updateEnvironment(localIndex, {
|
||||||
|
v: 1,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
variables: JSON.parse(variables),
|
variables: JSON.parse(variables),
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ import {
|
|||||||
deleteUserEnvironment,
|
deleteUserEnvironment,
|
||||||
updateUserEnvironment,
|
updateUserEnvironment,
|
||||||
} from "./environments.api"
|
} from "./environments.api"
|
||||||
|
import { SecretEnvironmentService } from "@hoppscotch/common/services/secret-environment.service"
|
||||||
|
import { getService } from "@hoppscotch/common/modules/dioc"
|
||||||
|
|
||||||
export const environmentsMapper = createMapper<number, string>()
|
export const environmentsMapper = createMapper<number, string>()
|
||||||
export const globalEnvironmentMapper = createMapper<number, string>()
|
export const globalEnvironmentMapper = createMapper<number, string>()
|
||||||
|
|
||||||
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
|
|
||||||
export const storeSyncDefinition: StoreSyncDefinitionOf<
|
export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||||
typeof environmentsStore
|
typeof environmentsStore
|
||||||
> = {
|
> = {
|
||||||
@@ -34,6 +38,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||||||
|
|
||||||
if (E.isRight(res)) {
|
if (E.isRight(res)) {
|
||||||
const id = res.right.createUserEnvironment.id
|
const id = res.right.createUserEnvironment.id
|
||||||
|
|
||||||
|
secretEnvironmentService.updateSecretEnvironmentID(
|
||||||
|
environmentsStore.value.environments[lastCreatedEnvIndex].id,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
|
||||||
environmentsStore.value.environments[lastCreatedEnvIndex].id = id
|
environmentsStore.value.environments[lastCreatedEnvIndex].id = id
|
||||||
removeDuplicateEntry(id)
|
removeDuplicateEntry(id)
|
||||||
}
|
}
|
||||||
@@ -84,7 +94,6 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||||||
},
|
},
|
||||||
updateEnvironment({ envIndex, updatedEnv }) {
|
updateEnvironment({ envIndex, updatedEnv }) {
|
||||||
const backendId = environmentsStore.value.environments[envIndex].id
|
const backendId = environmentsStore.value.environments[envIndex].id
|
||||||
|
|
||||||
if (backendId) {
|
if (backendId) {
|
||||||
updateUserEnvironment(backendId, updatedEnv)()
|
updateUserEnvironment(backendId, updatedEnv)()
|
||||||
}
|
}
|
||||||
@@ -97,7 +106,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||||||
setGlobalVariables({ entries }) {
|
setGlobalVariables({ entries }) {
|
||||||
const backendId = getGlobalVariableID()
|
const backendId = getGlobalVariableID()
|
||||||
if (backendId) {
|
if (backendId) {
|
||||||
updateUserEnvironment(backendId, { name: "", variables: entries })()
|
updateUserEnvironment(backendId, {
|
||||||
|
name: "",
|
||||||
|
variables: entries,
|
||||||
|
id: "",
|
||||||
|
v: 1,
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearGlobalVariables() {
|
clearGlobalVariables() {
|
||||||
|
|||||||
Reference in New Issue
Block a user