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_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"hide_secret": "Hide secret",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"less": "Less",
|
||||
@@ -43,6 +44,7 @@
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"show_secret": "Show secret",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -238,6 +240,7 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"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_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
@@ -269,6 +272,8 @@
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"secret": "Secret",
|
||||
"secret_value": "Secret value",
|
||||
"select": "Select environment",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
@@ -277,6 +282,7 @@
|
||||
"updated": "Environment updated",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"variables":"Variables",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
@@ -413,6 +419,8 @@
|
||||
"description": "Inspect possible errors",
|
||||
"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."
|
||||
},
|
||||
"header": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||
t("environment.value")
|
||||
}}</label>
|
||||
<input
|
||||
<SmartEnvInput
|
||||
v-model="editingValue"
|
||||
type="text"
|
||||
class="input"
|
||||
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
|
||||
addGlobalEnvVariable({
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
secret: false,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else if (scope.value.type === "my-environment") {
|
||||
addEnvironmentVariable(scope.value.index, {
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
secret: false,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ref } from "vue"
|
||||
|
||||
@@ -340,13 +340,13 @@ const showImportFailedError = () => {
|
||||
|
||||
const handleImportToStore = async (
|
||||
environments: Environment[],
|
||||
globalEnv?: Environment
|
||||
globalEnv?: NonSecretEnvironment
|
||||
) => {
|
||||
// if there's a global env, add them to the store
|
||||
if (globalEnv) {
|
||||
globalEnv.variables.forEach(({ key, value }) => {
|
||||
addGlobalEnvVariable({ key, value })
|
||||
})
|
||||
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||
addGlobalEnvVariable({ key, value, secret })
|
||||
)
|
||||
}
|
||||
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
|
||||
@@ -210,7 +210,10 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||
@@ -265,7 +268,10 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<div
|
||||
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
|
||||
type: "MY_ENV",
|
||||
index: props.modelValue.index,
|
||||
name: props.modelValue.environment?.name,
|
||||
variables: props.modelValue.environment?.variables,
|
||||
}
|
||||
} else if (props.modelValue?.type === "team-environment") {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: props.modelValue.environment.environment.name,
|
||||
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") {
|
||||
const environment =
|
||||
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
|
||||
})
|
||||
|
||||
const editGlobalEnv = () => {
|
||||
invokeAction("modals.my.environment.edit", {
|
||||
envName: "Global",
|
||||
})
|
||||
invokeAction("modals.global.environment.update", {})
|
||||
}
|
||||
|
||||
const editEnv = () => {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:env-vars="envVars"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsAdd
|
||||
@@ -37,7 +39,7 @@
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="showConfirmRemoveEnvModal"
|
||||
:title="t('confirm.remove_team')"
|
||||
:title="`${t('confirm.remove_environment')}`"
|
||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||
@resolve="removeSelectedEnvironment()"
|
||||
/>
|
||||
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
|
||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
const globalEnvironment = computed(() => ({
|
||||
v: 1 as const,
|
||||
id: "Global",
|
||||
name: "Global",
|
||||
variables: globalEnv.value,
|
||||
}))
|
||||
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const editingVariableValue = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const position = ref({ top: 0, left: 0 })
|
||||
|
||||
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
const editEnvironment = (environmentIndex: "Global") => {
|
||||
editingEnvironmentIndex.value = environmentIndex
|
||||
action.value = "edit"
|
||||
editingVariableName.value = ""
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
|
||||
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
editingVariableValue.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler("modals.environment.new", () => {
|
||||
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
||||
showConfirmRemoveEnvModal.value = true
|
||||
})
|
||||
|
||||
const additionalVars = ref<Environment["variables"]>([])
|
||||
|
||||
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
envName === "Global" && editEnvironment("Global")
|
||||
"modals.global.environment.update",
|
||||
({ variables, isSecret }) => {
|
||||
if (variables) {
|
||||
additionalVars.value = variables
|
||||
}
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
editEnvironment("Global")
|
||||
editingVariableName.value = "Global"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -16,76 +16,103 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<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 class="my-4 flex flex-col border border-divider rounded">
|
||||
<div
|
||||
v-for="({ id, env }, index) in vars"
|
||||
:key="`variable-${id}-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
v-if="evnExpandError"
|
||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||
>
|
||||
<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"
|
||||
: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>
|
||||
{{ t("environment.nested_overflow") }}
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||
<template #actions>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { ComputedRef, computed, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { environmentsStore } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
key: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +186,7 @@ const props = withDefaults(
|
||||
action: "edit" | "new"
|
||||
editingEnvironmentIndex?: number | "Global" | null
|
||||
editingVariableName?: string | null
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -162,6 +194,7 @@ const props = withDefaults(
|
||||
action: "edit",
|
||||
editingEnvironmentIndex: null,
|
||||
editingVariableName: null,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -172,11 +205,55 @@ const emit = defineEmits<{
|
||||
|
||||
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 editingID = ref<string>("")
|
||||
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>(
|
||||
IconTrash2,
|
||||
1000
|
||||
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||
|
||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
type SelectedEnv = "variables" | "secret"
|
||||
|
||||
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||
|
||||
const workingEnv = computed(() => {
|
||||
if (props.editingEnvironmentIndex === "Global") {
|
||||
const vars =
|
||||
props.editingVariableName === "Global"
|
||||
? props.envVars()
|
||||
: getGlobalVariables()
|
||||
return {
|
||||
name: "Global",
|
||||
variables: getGlobalVariables(),
|
||||
variables: vars,
|
||||
} as Environment
|
||||
} else if (props.action === "new") {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: "",
|
||||
variables: props.envVars(),
|
||||
}
|
||||
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
|
||||
|
||||
return pipe(
|
||||
variables,
|
||||
A.filter(({ secret }) => !secret),
|
||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||
)
|
||||
})
|
||||
@@ -239,11 +326,29 @@ watch(
|
||||
(show) => {
|
||||
if (show) {
|
||||
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(
|
||||
workingEnv.value?.variables ?? [],
|
||||
A.map((e) => ({
|
||||
A.mapWithIndex((index, e) => ({
|
||||
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 = () => {
|
||||
vars.value = []
|
||||
vars.value = vars.value.filter((e) =>
|
||||
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||
)
|
||||
|
||||
clearIcon.value = IconDone
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
}
|
||||
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const filterdVariables = pipe(
|
||||
const filteredVariables = pipe(
|
||||
vars.value,
|
||||
A.filterMap(
|
||||
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 = {
|
||||
v: 1,
|
||||
id: uniqueId(),
|
||||
name: editingName.value,
|
||||
variables: filterdVariables,
|
||||
variables,
|
||||
}
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
||||
createEnvironment(
|
||||
editingName.value,
|
||||
environmentUpdated.variables,
|
||||
editingID.value
|
||||
)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -150,6 +152,8 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const exportEnvironmentAsJSON = () => {
|
||||
const { environment, environmentIndex } = props
|
||||
exportAsJSON(environment, environmentIndex)
|
||||
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
|
||||
if (props.environmentIndex === null) return
|
||||
if (props.environmentIndex !== "Global") {
|
||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||
}
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<number | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
}
|
||||
)
|
||||
if (envName !== "Global") editEnvironment(envIndex)
|
||||
if (envName !== "Global") {
|
||||
editEnvironment(envIndex)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -16,90 +16,112 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<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 class="my-4 flex flex-col border border-divider rounded">
|
||||
<div
|
||||
v-for="({ id, env }, index) in vars"
|
||||
:key="`variable-${id}-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
v-if="evnExpandError"
|
||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||
>
|
||||
<input
|
||||
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>
|
||||
{{ t("environment.nested_overflow") }}
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||
<template #actions>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!isViewer"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="clearIcon"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!isViewer"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlus"
|
||||
:title="t('add.new')"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
<template v-if="!isViewer" #footer>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
@@ -119,7 +141,7 @@
|
||||
</template>
|
||||
|
||||
<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 A from "fp-ts/Array"
|
||||
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 IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { platform } from "~/platform"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +189,7 @@ const props = withDefaults(
|
||||
editingTeamId: string | undefined
|
||||
editingVariableName?: string | null
|
||||
isViewer?: boolean
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -172,6 +199,7 @@ const props = withDefaults(
|
||||
editingTeamId: "",
|
||||
editingVariableName: null,
|
||||
isViewer: false,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -182,11 +210,59 @@ const emit = defineEmits<{
|
||||
|
||||
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 editingID = ref<string | null>(null)
|
||||
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>(
|
||||
IconTrash2,
|
||||
1000
|
||||
@@ -215,22 +291,34 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = props.editingEnvironment?.environment.name ?? null
|
||||
selectedEnvOption.value = props.isSecretOptionSelected
|
||||
? "secret"
|
||||
: "variables"
|
||||
if (props.action === "new") {
|
||||
editingName.value = null
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
A.map((e) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
editingID.value = props.editingEnvironment.id
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
A.mapWithIndex((index, e) => ({
|
||||
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: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingTeamId,
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
hideModal()
|
||||
toast.success(`${t("environment.created")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
if (!props.isViewer) {
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(environmentUpdated.variables),
|
||||
props.editingTeamId,
|
||||
environmentUpdated.name
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
isLoading.value = false
|
||||
},
|
||||
(res) => {
|
||||
const envID = res.createTeamEnvironment.id
|
||||
if (envID) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
envID,
|
||||
secretVariables
|
||||
)
|
||||
}
|
||||
hideModal()
|
||||
toast.success(`${t("environment.created")}`)
|
||||
isLoading.value = false
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
} else {
|
||||
if (!props.editingEnvironment) {
|
||||
console.error("No Environment Found")
|
||||
return
|
||||
}
|
||||
|
||||
await pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingEnvironment.id,
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
hideModal()
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
}
|
||||
if (editingID.value) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
editingID.value,
|
||||
secretVariables
|
||||
)
|
||||
)()
|
||||
|
||||
// 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
|
||||
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
</span>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="!isViewer"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
@@ -57,6 +56,7 @@
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -69,6 +69,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
@@ -81,6 +82,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -137,6 +141,8 @@ const emit = defineEmits<{
|
||||
(e: "edit-environment"): void
|
||||
}>()
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () =>
|
||||
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("team_environment.deleted")}`)
|
||||
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
:editing-environment="editingEnvironment"
|
||||
:editing-team-id="team?.id"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
:is-viewer="team?.myRole === 'VIEWER'"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||
|
||||
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironment.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
||||
if (teamEnvToEdit) {
|
||||
editEnvironment(teamEnvToEdit)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -187,6 +187,8 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
const requestCode = computed(() => {
|
||||
const aggregateEnvs = getAggregateEnvs()
|
||||
const env: Environment = {
|
||||
v: 1,
|
||||
id: "env",
|
||||
name: "Env",
|
||||
variables: aggregateEnvs,
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ const clearContent = () => {
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||
|
||||
const computedHeaders = computed(() =>
|
||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
||||
getComputedHeaders(request.value, aggregateEnvs.value, false).map(
|
||||
(header, index) => ({
|
||||
id: `header-${index}`,
|
||||
...header,
|
||||
@@ -606,7 +606,8 @@ const inheritedProperties = computed(() => {
|
||||
const computedAuthHeader = getComputedAuthHeaders(
|
||||
aggregateEnvs.value,
|
||||
request.value,
|
||||
props.inheritedProperties.auth.inheritedAuth
|
||||
props.inheritedProperties.auth.inheritedAuth,
|
||||
false
|
||||
)[0]
|
||||
|
||||
if (
|
||||
|
||||
@@ -112,7 +112,7 @@ const handleAccessTokenRequest = async () => {
|
||||
}
|
||||
|
||||
const envs = getCombinedEnvVariables()
|
||||
const envVars = [...envs.selected, ...envs.global]
|
||||
const envVars = [...envs.selected.variables, ...envs.global]
|
||||
|
||||
try {
|
||||
const tokenReqParams = {
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
:icon="IconShare2"
|
||||
:label="t('tab.share_tab_request')"
|
||||
:shortcut="['S']"
|
||||
:new="true"
|
||||
@click="
|
||||
() => {
|
||||
emit('share-tab-request')
|
||||
|
||||
@@ -211,7 +211,6 @@ import { useI18n } from "@composables/i18n"
|
||||
import {
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setGlobalEnvVariables,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
@@ -225,6 +224,7 @@ import { useColorMode } from "~/composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
@@ -304,9 +304,10 @@ const globalHasAdditions = computed(() => {
|
||||
|
||||
const addEnvToGlobal = () => {
|
||||
if (!testResults.value?.envDiff.selected.additions) return
|
||||
setGlobalEnvVariables([
|
||||
...globalEnvVars.value,
|
||||
...testResults.value.envDiff.selected.additions,
|
||||
])
|
||||
|
||||
invokeAction("modals.global.environment.update", {
|
||||
variables: testResults.value.envDiff.selected.additions,
|
||||
isSecret: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
<div
|
||||
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
|
||||
v-else
|
||||
ref="editor"
|
||||
:placeholder="placeholder"
|
||||
class="flex flex-1"
|
||||
@@ -11,7 +22,14 @@
|
||||
@click="emit('click', $event)"
|
||||
@keydown="handleKeystroke"
|
||||
@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
|
||||
:inspection-results="inspectionResults"
|
||||
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 { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvsWithSecrets$,
|
||||
} from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||
import { InspectorResult } from "~/services/inspection"
|
||||
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(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
styles?: string
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
envs?: Env[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
@@ -80,6 +109,7 @@ const props = withDefaults(
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
contextMenuEnabled?: boolean
|
||||
secret?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -93,6 +123,7 @@ const props = withDefaults(
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
contextMenuEnabled: true,
|
||||
secret: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -118,10 +149,27 @@ const showSuggestionPopover = ref(false)
|
||||
const suggestionsMenu = 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, () => {
|
||||
showSuggestionPopover.value = false
|
||||
})
|
||||
|
||||
const toggleSecret = () => {
|
||||
isSecret.value = !isSecret.value
|
||||
}
|
||||
|
||||
//filter autocompleteSource with unique values
|
||||
const uniqueAutoCompleteSource = computed(() => {
|
||||
if (props.autoCompleteSource) {
|
||||
@@ -169,8 +217,6 @@ watch(
|
||||
)
|
||||
|
||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||
if (!props.autoCompleteSource) return
|
||||
|
||||
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
@@ -307,19 +353,28 @@ watch(
|
||||
let clipboardEv: ClipboardEvent | null = null
|
||||
let pastedValue: string | null = null
|
||||
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
||||
AggregateEnvironment[]
|
||||
>
|
||||
|
||||
const envVars = computed(() =>
|
||||
props.envs
|
||||
? props.envs.map((x) => ({
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: x.source,
|
||||
}))
|
||||
const envVars = computed(() => {
|
||||
return props.envs
|
||||
? props.envs.map((x) => {
|
||||
if (x.secret) {
|
||||
return {
|
||||
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
|
||||
)
|
||||
})
|
||||
|
||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||
|
||||
@@ -363,17 +418,28 @@ const initView = (el: any) => {
|
||||
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 = [
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (props.readonly) {
|
||||
if (readonly) {
|
||||
update.view.contentDOM.inputMode = "none"
|
||||
}
|
||||
}),
|
||||
EditorState.changeFilter.of(() => !props.readonly),
|
||||
EditorState.changeFilter.of(() => !readonly),
|
||||
inputTheme,
|
||||
props.readonly
|
||||
readonly
|
||||
? EditorView.theme({
|
||||
".cm-content": {
|
||||
caretColor: "var(--secondary-dark-color)",
|
||||
@@ -384,6 +450,7 @@ const initView = (el: any) => {
|
||||
})
|
||||
: EditorView.theme({}),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
props.environmentHighlights ? envTooltipPlugin : [],
|
||||
@@ -405,7 +472,8 @@ const initView = (el: any) => {
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
if (props.readonly) return
|
||||
if (readonly) return
|
||||
|
||||
if (update.docChanged) {
|
||||
const prevValue = clone(cachedValue.value)
|
||||
|
||||
@@ -454,14 +522,7 @@ const initView = (el: any) => {
|
||||
history(),
|
||||
keymap.of([...historyKeymap]),
|
||||
]
|
||||
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
return extensions
|
||||
}
|
||||
|
||||
const triggerTextSelection = () => {
|
||||
@@ -474,11 +535,11 @@ const triggerTextSelection = () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
if (props.selectTextOnMount) triggerTextSelection()
|
||||
if (props.focus) view.value?.focus()
|
||||
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
placeholder,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
Extension,
|
||||
@@ -269,6 +270,7 @@ export function useCodemirror(
|
||||
basicSetup,
|
||||
baseTheme,
|
||||
syntaxHighlighting(baseHighlightStyle, { fallback: true }),
|
||||
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
@@ -318,6 +320,7 @@ export function useCodemirror(
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
EditorView.domEventHandlers({
|
||||
scroll(event) {
|
||||
if (event.target && options.contextMenuEnabled) {
|
||||
@@ -359,6 +362,10 @@ export function useCodemirror(
|
||||
run: indentLess,
|
||||
},
|
||||
]),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
additionalExts.of(options.additionalExts ?? []),
|
||||
]
|
||||
|
||||
@@ -30,6 +30,13 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import {
|
||||
SecretEnvironmentService,
|
||||
SecretVariable,
|
||||
} from "~/services/secret-environment.service"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -58,15 +65,63 @@ const getTestableBody = (
|
||||
return x
|
||||
}
|
||||
|
||||
const combineEnvVariables = (env: {
|
||||
const combineEnvVariables = (envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}) => [...env.selected, ...env.global]
|
||||
}) => [...envs.selected, ...envs.global]
|
||||
|
||||
export const executedResponses$ = new Subject<
|
||||
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$(
|
||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||
): [
|
||||
@@ -154,15 +209,36 @@ export function runRESTRequest$(
|
||||
)
|
||||
|
||||
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
|
||||
tab.value.document.response = res
|
||||
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
const updatedRunResult = {
|
||||
...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 (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
@@ -173,8 +249,10 @@ export function runRESTRequest$(
|
||||
updateEnvironment(
|
||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
{
|
||||
...env,
|
||||
variables: runResult.right.envs.selected,
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: env.id ?? "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -186,7 +264,7 @@ export function runRESTRequest$(
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(runResult.right.envs.selected),
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
@@ -275,7 +353,6 @@ function translateToSandboxTestResults(
|
||||
|
||||
const globals = cloneDeep(getGlobalVariables())
|
||||
const env = getCurrentEnvironment()
|
||||
|
||||
return {
|
||||
description: "",
|
||||
expectResults: testDesc.tests.expectResults,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
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 { HoppGQLSaveContext } from "./graphql/document"
|
||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||
@@ -43,6 +43,7 @@ export type HoppAction =
|
||||
| "modals.environment.new" // Add new environment
|
||||
| "modals.environment.delete-selected" // Delete Selected 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.new" // Add new team
|
||||
| "modals.team.edit" // Edit selected team
|
||||
@@ -93,13 +94,19 @@ type HoppActionArgsMap = {
|
||||
}
|
||||
text: string | null
|
||||
}
|
||||
"modals.global.environment.update": {
|
||||
variables?: Environment["variables"]
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.my.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.delete": {
|
||||
teamId: string
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
|
||||
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
|
||||
mutation CreateTeamEnvironment(
|
||||
$variables: String!
|
||||
$teamID: ID!
|
||||
$name: String!
|
||||
) {
|
||||
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
|
||||
variables
|
||||
name
|
||||
teamID
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { StreamSubscriberFunc } from "@composables/stream"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvs$,
|
||||
getAggregateEnvs,
|
||||
aggregateEnvsWithSecrets$,
|
||||
getAggregateEnvsWithSecrets,
|
||||
getSelectedEnvironmentType,
|
||||
} from "~/newstore/environments"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
@@ -66,7 +66,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
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)
|
||||
|
||||
@@ -89,6 +98,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
invokeAction(`modals.${action}.environment.edit`, {
|
||||
envName,
|
||||
variableName: parsedEnvKey,
|
||||
isSecret: tooltipEnv?.secret,
|
||||
})
|
||||
})
|
||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||
@@ -171,9 +181,9 @@ export class HoppEnvironmentPlugin {
|
||||
subscribeToStream: StreamSubscriberFunc,
|
||||
private editorView: Ref<EditorView | undefined>
|
||||
) {
|
||||
this.envs = getAggregateEnvs()
|
||||
this.envs = getAggregateEnvsWithSecrets()
|
||||
|
||||
subscribeToStream(aggregateEnvs$, (envs) => {
|
||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||
this.envs = envs
|
||||
|
||||
this.editorView.value?.dispatch({
|
||||
|
||||
@@ -12,8 +12,6 @@ const getEnvironmentJson = (
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
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 { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
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) => {
|
||||
const parsedContent = safeParseJSON(content, true)
|
||||
|
||||
@@ -25,7 +16,9 @@ export const hoppEnvImporter = (content: string) => {
|
||||
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) {
|
||||
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 { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const insomniaResourcesSchema = z.object({
|
||||
resources: z.array(
|
||||
@@ -56,16 +57,18 @@ export const insomniaEnvImporter = (content: string) => {
|
||||
return { ...envResource, data: stringifiedData }
|
||||
})
|
||||
|
||||
const environments: Environment[] = []
|
||||
const environments: NonSecretEnvironment[] = []
|
||||
|
||||
insomniaEnvs.forEach((insomniaEnv) => {
|
||||
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
||||
|
||||
if (parsedInsomniaEnv.success) {
|
||||
const environment: Environment = {
|
||||
const environment: NonSecretEnvironment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: parsedInsomniaEnv.data.name,
|
||||
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 { Environment } from "@hoppscotch/data"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const postmanEnvSchema = z.object({
|
||||
name: z.string(),
|
||||
@@ -34,12 +35,14 @@ export const postmanEnvImporter = (content: string) => {
|
||||
const postmanEnv = validationResult.data
|
||||
|
||||
const environment: Environment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: postmanEnv.name,
|
||||
variables: [],
|
||||
}
|
||||
|
||||
postmanEnv.values.forEach(({ key, value }) =>
|
||||
environment.variables.push({ key, value })
|
||||
environment.variables.push({ key, value, secret: false })
|
||||
)
|
||||
|
||||
return TE.right(environment)
|
||||
|
||||
@@ -8,11 +8,71 @@ import {
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
export const getCombinedEnvVariables = () => ({
|
||||
global: cloneDeep(getGlobalVariables()),
|
||||
selected: cloneDeep(getCurrentEnvironment().variables),
|
||||
})
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
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 = (
|
||||
script: string,
|
||||
|
||||
@@ -118,6 +118,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
@@ -196,6 +198,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
@@ -249,6 +253,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
|
||||
@@ -45,7 +45,8 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
export const getComputedAuthHeaders = (
|
||||
envVars: Environment["variables"],
|
||||
req?: HoppRESTRequest,
|
||||
auth?: HoppRESTRequest["auth"]
|
||||
auth?: HoppRESTRequest["auth"],
|
||||
parse = true
|
||||
) => {
|
||||
const request = auth ? { auth: auth ?? { authActive: false } } : req
|
||||
// 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 ?
|
||||
if (request.auth.authType === "basic") {
|
||||
const username = parseTemplateString(request.auth.username, envVars)
|
||||
const password = parseTemplateString(request.auth.password, envVars)
|
||||
const username = parse
|
||||
? parseTemplateString(request.auth.username, envVars)
|
||||
: request.auth.username
|
||||
const password = parse
|
||||
? parseTemplateString(request.auth.password, envVars)
|
||||
: request.auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
@@ -75,7 +80,11 @@ export const getComputedAuthHeaders = (
|
||||
headers.push({
|
||||
active: true,
|
||||
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") {
|
||||
const { key, addTo } = request.auth
|
||||
@@ -83,7 +92,9 @@ export const getComputedAuthHeaders = (
|
||||
headers.push({
|
||||
active: true,
|
||||
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 = (
|
||||
req: HoppRESTRequest,
|
||||
envVars: Environment["variables"]
|
||||
envVars: Environment["variables"],
|
||||
parse = true
|
||||
): ComputedHeader[] => {
|
||||
return [
|
||||
...getComputedAuthHeaders(envVars, req).map((header) => ({
|
||||
...getComputedAuthHeaders(envVars, req, undefined, parse).map((header) => ({
|
||||
source: "auth" as const,
|
||||
header,
|
||||
})),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { cloneDeep, isEqual } from "lodash-es"
|
||||
import { cloneDeep, isEqual, uniqueId } from "lodash-es"
|
||||
import { combineLatest, Observable } from "rxjs"
|
||||
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import DispatchingStore, {
|
||||
defineDispatchers,
|
||||
} from "~/newstore/DispatchingStore"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
export type SelectedEnvironmentIndex =
|
||||
| { type: "NO_ENV_SELECTED" }
|
||||
@@ -19,6 +21,8 @@ export type SelectedEnvironmentIndex =
|
||||
const defaultEnvironmentsState = {
|
||||
environments: [
|
||||
{
|
||||
v: 1,
|
||||
id: uniqueId(),
|
||||
name: "My Environment Variables",
|
||||
variables: [],
|
||||
},
|
||||
@@ -33,6 +37,8 @@ const defaultEnvironmentsState = {
|
||||
} as SelectedEnvironmentIndex,
|
||||
}
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
type EnvironmentStore = typeof defaultEnvironmentsState
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
@@ -88,10 +94,13 @@ const dispatchers = defineDispatchers({
|
||||
envID
|
||||
? {
|
||||
id: envID,
|
||||
v: 1,
|
||||
name,
|
||||
variables,
|
||||
}
|
||||
: {
|
||||
v: 1,
|
||||
id: "",
|
||||
name,
|
||||
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 {
|
||||
environments: [
|
||||
...environments,
|
||||
{
|
||||
...cloneDeep(newEnvironment),
|
||||
id: uniqueId(),
|
||||
name: `${newEnvironment.name} - Duplicate`,
|
||||
},
|
||||
],
|
||||
@@ -184,14 +191,19 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
addEnvironmentVariable(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex, key, value }: { envIndex: number; key: string; value: string }
|
||||
{
|
||||
envIndex,
|
||||
key,
|
||||
value,
|
||||
secret,
|
||||
}: { envIndex: number; key: string; value: string; secret: boolean }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: [...env.variables, { key, value }],
|
||||
variables: [...env.variables, { key, value, secret }],
|
||||
}
|
||||
: env
|
||||
),
|
||||
@@ -219,7 +231,10 @@ const dispatchers = defineDispatchers({
|
||||
{
|
||||
envIndex,
|
||||
vars,
|
||||
}: { envIndex: number; vars: { key: string; value: string }[] }
|
||||
}: {
|
||||
envIndex: number
|
||||
vars: { key: string; value: string; secret: boolean }[]
|
||||
}
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
@@ -253,7 +268,7 @@ const dispatchers = defineDispatchers({
|
||||
...env,
|
||||
variables: env.variables.map((v, vIndex) =>
|
||||
vIndex === variableIndex
|
||||
? { key: updatedKey, value: updatedValue }
|
||||
? { key: updatedKey, value: updatedValue, secret: v.secret }
|
||||
: v
|
||||
),
|
||||
}
|
||||
@@ -343,6 +358,8 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||
const env: Environment = {
|
||||
name: "No environment",
|
||||
v: 1,
|
||||
id: "",
|
||||
variables: [],
|
||||
}
|
||||
return env
|
||||
@@ -356,6 +373,7 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
||||
export type AggregateEnvironment = {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
sourceEnv: string
|
||||
}
|
||||
|
||||
@@ -370,11 +388,11 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
||||
map(([selectedEnv, globalVars]) => {
|
||||
const results: AggregateEnvironment[] = []
|
||||
|
||||
selectedEnv?.variables.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
||||
selectedEnv?.variables.forEach(({ key, value, secret }) =>
|
||||
results.push({ key, value, secret, sourceEnv: selectedEnv.name })
|
||||
)
|
||||
globalVars.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: "Global" })
|
||||
globalVars.forEach(({ key, value, secret }) =>
|
||||
results.push({ key, value, secret, sourceEnv: "Global" })
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -384,32 +402,129 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
||||
|
||||
export function getAggregateEnvs() {
|
||||
const currentEnv = getCurrentEnvironment()
|
||||
|
||||
return [
|
||||
...currentEnv.variables.map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: currentEnv.name,
|
||||
}
|
||||
),
|
||||
...getGlobalVariables().map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: "Global",
|
||||
}
|
||||
),
|
||||
...currentEnv.variables.map((x) => {
|
||||
let value
|
||||
if (!x.secret) {
|
||||
value = x.value
|
||||
}
|
||||
|
||||
return <AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value,
|
||||
secret: x.secret,
|
||||
sourceEnv: currentEnv.name,
|
||||
}
|
||||
}),
|
||||
...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 {
|
||||
if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
|
||||
) {
|
||||
return {
|
||||
v: 1,
|
||||
id: "",
|
||||
name: "No environment",
|
||||
variables: [],
|
||||
}
|
||||
@@ -589,7 +704,7 @@ export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
|
||||
|
||||
export function setEnvironmentVariables(
|
||||
envIndex: number,
|
||||
vars: { key: string; value: string }[]
|
||||
vars: { key: string; value: string; secret: boolean }[]
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "setEnvironmentVariables",
|
||||
@@ -602,7 +717,7 @@ export function setEnvironmentVariables(
|
||||
|
||||
export function addEnvironmentVariable(
|
||||
envIndex: number,
|
||||
{ key, value }: { key: string; value: string }
|
||||
{ key, value, secret }: { key: string; value: string; secret: boolean }
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "addEnvironmentVariable",
|
||||
@@ -610,6 +725,7 @@ export function addEnvironmentVariable(
|
||||
envIndex,
|
||||
key,
|
||||
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 {
|
||||
__esModule: true,
|
||||
aggregateEnvs$: new BehaviorSubject([
|
||||
{ key: "EXISTING_ENV_VAR", value: "test_value" },
|
||||
aggregateEnvsWithSecrets$: new BehaviorSubject([
|
||||
{ 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.objectContaining({
|
||||
id: "environment",
|
||||
id: "environment-not-found-0",
|
||||
isApplicable: true,
|
||||
text: {
|
||||
type: "text",
|
||||
@@ -91,7 +103,7 @@ describe("EnvironmentInspectorService", () => {
|
||||
|
||||
expect(result.value).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "environment",
|
||||
id: "environment-not-found-0",
|
||||
isApplicable: true,
|
||||
text: {
|
||||
type: "text",
|
||||
@@ -134,7 +146,7 @@ describe("EnvironmentInspectorService", () => {
|
||||
|
||||
expect(result.value).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "environment",
|
||||
id: "environment-not-found-0",
|
||||
isApplicable: true,
|
||||
text: {
|
||||
type: "text",
|
||||
@@ -161,5 +173,103 @@ describe("EnvironmentInspectorService", () => {
|
||||
|
||||
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 IconPlusCircle from "~icons/lucide/plus-circle"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { aggregateEnvs$ } from "~/newstore/environments"
|
||||
import {
|
||||
aggregateEnvsWithSecrets$,
|
||||
getCurrentEnvironment,
|
||||
getSelectedEnvironmentType,
|
||||
} from "~/newstore/environments"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { computed } from "vue"
|
||||
import { useStreamStatic } from "~/composables/stream"
|
||||
@@ -36,9 +40,13 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
|
||||
private readonly inspection = this.bind(InspectionService)
|
||||
|
||||
private aggregateEnvs = useStreamStatic(aggregateEnvs$, [], () => {
|
||||
/* noop */
|
||||
})[0]
|
||||
private aggregateEnvsWithSecrets = useStreamStatic(
|
||||
aggregateEnvsWithSecrets$,
|
||||
[],
|
||||
() => {
|
||||
/* noop */
|
||||
}
|
||||
)[0]
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -49,9 +57,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
/**
|
||||
* Validates the environment variables in the target array
|
||||
* @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
|
||||
* @returns The results array
|
||||
* @returns The results array containing the results of the validation
|
||||
*/
|
||||
private validateEnvironmentVariables = (
|
||||
target: any[],
|
||||
@@ -59,7 +66,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
) => {
|
||||
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) => {
|
||||
if (isENVInString(element)) {
|
||||
@@ -68,29 +75,20 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
if (extractedEnv) {
|
||||
extractedEnv.forEach((exEnv: string) => {
|
||||
const formattedExEnv = exEnv.slice(2, -2)
|
||||
let itemLocation: InspectorLocation
|
||||
if (locations.type === "header") {
|
||||
itemLocation = {
|
||||
type: "header",
|
||||
position: locations.position,
|
||||
index: index,
|
||||
key: element,
|
||||
}
|
||||
} else if (locations.type === "parameter") {
|
||||
itemLocation = {
|
||||
type: "parameter",
|
||||
position: locations.position,
|
||||
index: index,
|
||||
key: element,
|
||||
}
|
||||
} else {
|
||||
itemLocation = {
|
||||
type: "url",
|
||||
}
|
||||
const itemLocation: InspectorLocation = {
|
||||
type: locations.type,
|
||||
position:
|
||||
locations.type === "url" ||
|
||||
locations.type === "body" ||
|
||||
locations.type === "response"
|
||||
? "key"
|
||||
: locations.position,
|
||||
index: index,
|
||||
key: element,
|
||||
}
|
||||
if (!envKeys.includes(formattedExEnv)) {
|
||||
newErrors.push({
|
||||
id: "environment",
|
||||
id: `environment-not-found-${newErrors.length}`,
|
||||
text: {
|
||||
type: "text",
|
||||
text: this.t("inspections.environment.not_found", {
|
||||
@@ -112,7 +110,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
locations: itemLocation,
|
||||
doc: {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>>) {
|
||||
return computed(() => {
|
||||
const results: InspectorResult[] = []
|
||||
@@ -132,12 +227,25 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
|
||||
const params = req.value.params
|
||||
|
||||
/**
|
||||
* Validate the environment variables in the URL
|
||||
*/
|
||||
const url = req.value.endpoint
|
||||
|
||||
results.push(
|
||||
...this.validateEnvironmentVariables([req.value.endpoint], {
|
||||
...this.validateEnvironmentVariables([url], {
|
||||
type: "url",
|
||||
})
|
||||
)
|
||||
results.push(
|
||||
...this.validateEmptyEnvironmentVariables([url], {
|
||||
type: "url",
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate the environment variables in the headers
|
||||
*/
|
||||
const headerKeys = Object.values(headers).map((header) => header.key)
|
||||
|
||||
results.push(
|
||||
@@ -146,6 +254,12 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
position: "key",
|
||||
})
|
||||
)
|
||||
results.push(
|
||||
...this.validateEmptyEnvironmentVariables(headerKeys, {
|
||||
type: "header",
|
||||
position: "key",
|
||||
})
|
||||
)
|
||||
|
||||
const headerValues = Object.values(headers).map((header) => header.value)
|
||||
|
||||
@@ -155,7 +269,16 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
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)
|
||||
|
||||
results.push(
|
||||
@@ -164,6 +287,12 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
||||
position: "key",
|
||||
})
|
||||
)
|
||||
results.push(
|
||||
...this.validateEmptyEnvironmentVariables(paramsKeys, {
|
||||
type: "parameter",
|
||||
position: "key",
|
||||
})
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export class HeaderInspectorService extends Service implements Inspector {
|
||||
},
|
||||
doc: {
|
||||
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: {
|
||||
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 { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
|
||||
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
|
||||
import { SecretVariable } from "~/services/secret-environment.service"
|
||||
import { PersistableTabState } from "~/services/tab"
|
||||
|
||||
type VUEX_DATA = {
|
||||
@@ -19,7 +20,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
||||
|
||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 1,
|
||||
v: 2,
|
||||
name: "Echo",
|
||||
folders: [],
|
||||
requests: [
|
||||
@@ -36,12 +37,14 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
body: { contentType: null, body: null },
|
||||
},
|
||||
],
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [],
|
||||
},
|
||||
]
|
||||
|
||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 1,
|
||||
v: 2,
|
||||
name: "Echo",
|
||||
folders: [],
|
||||
requests: [
|
||||
@@ -55,20 +58,30 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
auth: { authType: "none", authActive: true },
|
||||
},
|
||||
],
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [],
|
||||
},
|
||||
]
|
||||
|
||||
export const ENVIRONMENTS_MOCK: Environment[] = [
|
||||
{
|
||||
v: 1,
|
||||
id: "ENV_1",
|
||||
name: "globals",
|
||||
variables: [
|
||||
{
|
||||
key: "test-global-key",
|
||||
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 = {
|
||||
@@ -98,7 +111,7 @@ export const MQTT_REQUEST_MOCK = {
|
||||
}
|
||||
|
||||
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 = {
|
||||
@@ -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_HISTORY_MOCK,
|
||||
REST_TAB_STATE_MOCK,
|
||||
SECRET_ENVIRONMENTS_MOCK,
|
||||
SELECTED_ENV_INDEX_MOCK,
|
||||
SOCKET_IO_REQUEST_MOCK,
|
||||
SSE_REQUEST_MOCK,
|
||||
VUEX_DATA_MOCK,
|
||||
WEBSOCKET_REQUEST_MOCK,
|
||||
} from "./__mocks__"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
vi.mock("~/modules/i18n", () => {
|
||||
return {
|
||||
@@ -122,10 +124,12 @@ const spyOnSetItem = () => vi.spyOn(Storage.prototype, "setItem")
|
||||
const bindPersistenceService = ({
|
||||
mockGQLTabService = false,
|
||||
mockRESTTabService = false,
|
||||
mockSecretEnvironmentsService = false,
|
||||
mock = {},
|
||||
}: {
|
||||
mockGQLTabService?: boolean
|
||||
mockRESTTabService?: boolean
|
||||
mockSecretEnvironmentsService?: boolean
|
||||
mock?: Record<string, unknown>
|
||||
} = {}) => {
|
||||
const container = new TestContainer()
|
||||
@@ -138,6 +142,10 @@ const bindPersistenceService = ({
|
||||
container.bindMock(RESTTabService, mock)
|
||||
}
|
||||
|
||||
if (mockSecretEnvironmentsService) {
|
||||
container.bindMock(SecretEnvironmentService, mock)
|
||||
}
|
||||
|
||||
container.bind(PersistenceService)
|
||||
|
||||
const service = container.bind(PersistenceService)
|
||||
@@ -893,7 +901,12 @@ describe("PersistenceService", () => {
|
||||
// Invalid shape for `environments`
|
||||
const environments = [
|
||||
// `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(
|
||||
@@ -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", () => {
|
||||
// Key read from localStorage across test cases
|
||||
const wsRequestKey = "WebsocketRequest"
|
||||
|
||||
@@ -67,10 +67,12 @@ import {
|
||||
SETTINGS_SCHEMA,
|
||||
SOCKET_IO_REQUEST_SCHEMA,
|
||||
SSE_REQUEST_SCHEMA,
|
||||
SECRET_ENVIRONMENT_VARIABLE_SCHEMA,
|
||||
THEME_COLOR_SCHEMA,
|
||||
VUEX_SCHEMA,
|
||||
WEBSOCKET_REQUEST_SCHEMA,
|
||||
} from "./validation-schemas"
|
||||
import { SecretEnvironmentService } from "../secret-environment.service"
|
||||
|
||||
/**
|
||||
* 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 gqlTabService = this.bind(GQLTabService)
|
||||
|
||||
private readonly secretEnvironmentService = this.bind(
|
||||
SecretEnvironmentService
|
||||
)
|
||||
|
||||
public hoppLocalConfigStorage: StorageLike = localStorage
|
||||
|
||||
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() {
|
||||
const selectedEnvIndexKey = "selectedEnvIndex"
|
||||
let selectedEnvIndexValue = JSON.parse(
|
||||
@@ -697,6 +753,8 @@ export class PersistenceService extends Service {
|
||||
this.setupSocketIOPersistence()
|
||||
this.setupSSEPersistence()
|
||||
this.setupMQTTPersistence()
|
||||
|
||||
this.setupSecretEnvironmentsPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -160,7 +160,6 @@ export const SELECTED_ENV_INDEX_SCHEMA = z.nullable(
|
||||
type: z.literal("TEAM_ENV"),
|
||||
teamID: z.string(),
|
||||
teamEnvID: z.string(),
|
||||
// ! Versioned entity
|
||||
environment: entityReference(Environment),
|
||||
}),
|
||||
])
|
||||
@@ -212,13 +211,19 @@ export const MQTT_REQUEST_SCHEMA = z.nullable(
|
||||
|
||||
export const GLOBAL_ENV_SCHEMA = z.union([
|
||||
z.array(z.never()),
|
||||
|
||||
z.array(
|
||||
z
|
||||
.object({
|
||||
z.union([
|
||||
z.object({
|
||||
key: z.string(),
|
||||
secret: z.literal(true),
|
||||
}),
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
.strict()
|
||||
secret: z.literal(false),
|
||||
}),
|
||||
])
|
||||
),
|
||||
])
|
||||
|
||||
@@ -339,12 +344,34 @@ const HoppTestDataSchema = z.lazy(() =>
|
||||
.strict()
|
||||
)
|
||||
|
||||
const EnvironmentVariablesSchema = z
|
||||
.object({
|
||||
const EnvironmentVariablesSchema = z.union([
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
.strict()
|
||||
secret: z.literal(false),
|
||||
}),
|
||||
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
|
||||
.object({
|
||||
@@ -358,7 +385,11 @@ const HoppTestResultSchema = z
|
||||
.object({
|
||||
additions: z.array(EnvironmentVariablesSchema),
|
||||
updations: z.array(
|
||||
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
||||
EnvironmentVariablesSchema.refine((x) => !x.secret).and(
|
||||
z.object({
|
||||
previousValue: z.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
deletions: z.array(EnvironmentVariablesSchema),
|
||||
})
|
||||
@@ -367,7 +398,11 @@ const HoppTestResultSchema = z
|
||||
.object({
|
||||
additions: z.array(EnvironmentVariablesSchema),
|
||||
updations: z.array(
|
||||
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
|
||||
EnvironmentVariablesSchema.refine((x) => !x.secret).and(
|
||||
z.object({
|
||||
previousValue: z.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
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()
|
||||
break
|
||||
case "edit_global_env":
|
||||
invokeAction(`modals.my.environment.edit`, {
|
||||
envName: "Global",
|
||||
})
|
||||
invokeAction(`modals.global.environment.update`, {})
|
||||
break
|
||||
case "duplicate_global_env":
|
||||
this.duplicateGlobalEnv()
|
||||
|
||||
@@ -2,22 +2,38 @@ import * as E from "fp-ts/Either"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { InferredEntity, createVersionedEntity } from "verzod"
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
import V0_VERSION from "./v/0"
|
||||
import V1_VERSION from "./v/1"
|
||||
|
||||
const versionedObject = z.object({
|
||||
v: z.number(),
|
||||
})
|
||||
|
||||
export const Environment = createVersionedEntity({
|
||||
latestVersion: 0,
|
||||
latestVersion: 1,
|
||||
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 EnvironmentVariable = InferredEntity<
|
||||
typeof Environment
|
||||
>["variables"][number]
|
||||
|
||||
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
|
||||
|
||||
export const EnvironmentSchemaVersion = 1
|
||||
|
||||
export function parseBodyEnvVariablesE(
|
||||
body: string,
|
||||
env: Environment["variables"]
|
||||
@@ -43,7 +61,11 @@ export function parseBodyEnvVariablesE(
|
||||
const found = env.find(
|
||||
(envVar) => envVar.key === key.replace(/[<>]/g, "")
|
||||
)
|
||||
return found ? found.value : key
|
||||
|
||||
if (found && "value" in found) {
|
||||
return found.value
|
||||
}
|
||||
return key
|
||||
})
|
||||
|
||||
depth++
|
||||
@@ -68,7 +90,10 @@ export const parseBodyEnvVariables = (
|
||||
|
||||
export function parseTemplateStringE(
|
||||
str: string,
|
||||
variables: Environment["variables"]
|
||||
variables:
|
||||
| Environment["variables"]
|
||||
| { secret: true; value: string; key: string }[],
|
||||
maskValue = false
|
||||
) {
|
||||
if (!variables || !str) {
|
||||
return E.right(str)
|
||||
@@ -78,10 +103,21 @@ export function parseTemplateStringE(
|
||||
let depth = 0
|
||||
|
||||
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
|
||||
result = decodeURI(encodeURI(result)).replace(
|
||||
REGEX_ENV_VAR,
|
||||
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
|
||||
)
|
||||
result = decodeURI(encodeURI(result)).replace(REGEX_ENV_VAR, (_, p1) => {
|
||||
const variable = variables.find((x) => x && x.key === p1)
|
||||
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++
|
||||
}
|
||||
|
||||
@@ -90,14 +126,52 @@ export function parseTemplateStringE(
|
||||
: E.right(result)
|
||||
}
|
||||
|
||||
export type NonSecretEnvironmentVariable = Extract<
|
||||
EnvironmentVariable,
|
||||
{ secret: false }
|
||||
>
|
||||
|
||||
export type NonSecretEnvironment = Omit<Environment, "variables"> & {
|
||||
variables: NonSecretEnvironmentVariable[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `parseTemplateStringE` instead
|
||||
*/
|
||||
export const parseTemplateString = (
|
||||
str: string,
|
||||
variables: Environment["variables"]
|
||||
variables:
|
||||
| Environment["variables"]
|
||||
| { secret: true; value: string; key: string }[],
|
||||
maskValue = false
|
||||
) =>
|
||||
pipe(
|
||||
parseTemplateStringE(str, variables),
|
||||
parseTemplateStringE(str, variables, maskValue),
|
||||
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(),
|
||||
value: z.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export default defineVersion({
|
||||
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: [],
|
||||
selected: [
|
||||
{ key: "bob", value: "oldbob" },
|
||||
{ key: "foo", value: "bar" },
|
||||
{ key: "bob", value: "oldbob", secret: false },
|
||||
{ key: "foo", value: "bar", secret: false },
|
||||
],
|
||||
}
|
||||
)()
|
||||
).resolves.toEqualRight({
|
||||
global: [],
|
||||
selected: [
|
||||
{ key: "bob", value: "newbob" },
|
||||
{ key: "foo", value: "bar" },
|
||||
{ key: "bob", value: "newbob", secret: false },
|
||||
{ key: "foo", value: "bar", secret: false },
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -35,8 +35,8 @@ describe("runPreRequestScript", () => {
|
||||
{
|
||||
global: [],
|
||||
selected: [
|
||||
{ key: "bob", value: "oldbob" },
|
||||
{ key: "foo", value: "bar" },
|
||||
{ key: "bob", value: "oldbob", secret: false },
|
||||
{ key: "foo", value: "bar", secret: false },
|
||||
],
|
||||
}
|
||||
)()
|
||||
@@ -52,8 +52,8 @@ describe("runPreRequestScript", () => {
|
||||
{
|
||||
global: [],
|
||||
selected: [
|
||||
{ key: "bob", value: "oldbob" },
|
||||
{ key: "foo", value: "bar" },
|
||||
{ key: "bob", value: "oldbob", secret: false },
|
||||
{ key: "foo", value: "bar", secret: false },
|
||||
],
|
||||
}
|
||||
)()
|
||||
@@ -69,8 +69,8 @@ describe("runPreRequestScript", () => {
|
||||
{
|
||||
global: [],
|
||||
selected: [
|
||||
{ key: "bob", value: "oldbob" },
|
||||
{ key: "foo", value: "bar" },
|
||||
{ key: "bob", value: "oldbob", secret: false },
|
||||
{ key: "foo", value: "bar", secret: false },
|
||||
],
|
||||
}
|
||||
)()
|
||||
@@ -87,7 +87,7 @@ describe("runPreRequestScript", () => {
|
||||
)()
|
||||
).resolves.toEqualRight({
|
||||
global: [],
|
||||
selected: [{ key: "foo", value: "bar" }],
|
||||
selected: [{ key: "foo", value: "bar", secret: false }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,13 +10,13 @@ describe("Base64 helper functions", () => {
|
||||
atob: {
|
||||
script: `pw.env.set("atob", atob("SGVsbG8gV29ybGQ="))`,
|
||||
environment: {
|
||||
selected: [{ key: "atob", value: "Hello World" }],
|
||||
selected: [{ key: "atob", value: "Hello World", secret: false }],
|
||||
},
|
||||
},
|
||||
btoa: {
|
||||
script: `pw.env.set("btoa", btoa("Hello World"))`,
|
||||
environment: {
|
||||
selected: [{ key: "btoa", value: "SGVsbG8gV29ybGQ=" }],
|
||||
selected: [{ key: "btoa", value: "SGVsbG8gV29ybGQ=", secret: false }],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ describe("pw.env.get", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -59,6 +60,7 @@ describe("pw.env.get", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -112,12 +114,14 @@ describe("pw.env.get", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "global val",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "a",
|
||||
value: "selected val",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -147,6 +151,7 @@ describe("pw.env.get", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "<<hello>>",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ describe("pw.env.getResolve", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -59,6 +60,7 @@ describe("pw.env.getResolve", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -112,12 +114,14 @@ describe("pw.env.getResolve", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "global val",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "a",
|
||||
value: "selected val",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -147,10 +151,12 @@ describe("pw.env.getResolve", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "<<hello>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "hello",
|
||||
value: "there",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -180,10 +186,12 @@ describe("pw.env.getResolve", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "<<hello>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "hello",
|
||||
value: "<<a>>",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("pw.env.resolve", () => {
|
||||
{
|
||||
key: "hello",
|
||||
value: "there",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -73,6 +74,7 @@ describe("pw.env.resolve", () => {
|
||||
{
|
||||
key: "hello",
|
||||
value: "there",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -101,12 +103,14 @@ describe("pw.env.resolve", () => {
|
||||
{
|
||||
key: "hello",
|
||||
value: "yo",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "hello",
|
||||
value: "there",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -136,10 +140,12 @@ describe("pw.env.resolve", () => {
|
||||
{
|
||||
key: "hello",
|
||||
value: "<<there>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "there",
|
||||
value: "<<hello>>",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -45,6 +46,7 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "c",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -62,6 +64,7 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -73,6 +76,7 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "c",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -90,12 +94,14 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "a",
|
||||
value: "d",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -106,12 +112,14 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "b",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "a",
|
||||
value: "c",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -136,6 +144,7 @@ describe("pw.env.set", () => {
|
||||
{
|
||||
key: "a",
|
||||
value: "c",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -57,6 +58,7 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -80,12 +82,14 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://httpbin.org",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -96,6 +100,7 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://httpbin.org",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -114,16 +119,19 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://httpbin.org",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -134,12 +142,14 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -157,10 +167,12 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://httpbin.org/",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -172,6 +184,7 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
value: "https://echo.hoppscotch.io",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
@@ -225,6 +238,7 @@ describe("pw.env.unset", () => {
|
||||
{
|
||||
key: "baseUrl",
|
||||
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
|
||||
*/
|
||||
@@ -43,6 +41,13 @@ export type 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
|
||||
*/
|
||||
@@ -50,8 +55,8 @@ export type TestDescriptor = {
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
global: TransformedEnvironmentVariable[]
|
||||
selected: TransformedEnvironmentVariable[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,20 @@ const setEnv = (
|
||||
const indexInGlobal = findEnvIndex(envName, global)
|
||||
|
||||
if (indexInSelected >= 0) {
|
||||
selected[indexInSelected].value = envValue
|
||||
const selectedEnv = selected[indexInSelected]
|
||||
if ("value" in selectedEnv) {
|
||||
selectedEnv.value = envValue
|
||||
}
|
||||
} 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 {
|
||||
selected.push({
|
||||
key: envName,
|
||||
value: envValue,
|
||||
secret: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,9 +93,9 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, updatedEnvs),
|
||||
O.match(
|
||||
O.fold(
|
||||
() => undefined,
|
||||
({ value }) => String(value)
|
||||
(env) => String(env.value)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -104,14 +111,13 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
getEnv(key, updatedEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
E.map((e) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [
|
||||
parseTemplateStringE(e.value, [
|
||||
...updatedEnvs.selected,
|
||||
...updatedEnvs.global,
|
||||
]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
]), // If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => e.value)
|
||||
)
|
||||
),
|
||||
E.map((x) => String(x)),
|
||||
|
||||
@@ -81,6 +81,7 @@ async function loadUserEnvironments() {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
replaceEnvironments(
|
||||
environments.map(({ id, variables, name }) => ({
|
||||
v: 1,
|
||||
id,
|
||||
name,
|
||||
variables: JSON.parse(variables),
|
||||
@@ -164,6 +165,7 @@ function setupUserEnvironmentUpdatedSubscription() {
|
||||
if ((localIndex || localIndex == 0) && name) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
updateEnvironment(localIndex, {
|
||||
v: 1,
|
||||
id,
|
||||
name,
|
||||
variables: JSON.parse(variables),
|
||||
|
||||
@@ -20,10 +20,14 @@ import {
|
||||
deleteUserEnvironment,
|
||||
updateUserEnvironment,
|
||||
} 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 globalEnvironmentMapper = createMapper<number, string>()
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||
typeof environmentsStore
|
||||
> = {
|
||||
@@ -34,6 +38,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||
|
||||
if (E.isRight(res)) {
|
||||
const id = res.right.createUserEnvironment.id
|
||||
|
||||
secretEnvironmentService.updateSecretEnvironmentID(
|
||||
environmentsStore.value.environments[lastCreatedEnvIndex].id,
|
||||
id
|
||||
)
|
||||
|
||||
environmentsStore.value.environments[lastCreatedEnvIndex].id = id
|
||||
removeDuplicateEntry(id)
|
||||
}
|
||||
@@ -84,7 +94,6 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||
},
|
||||
updateEnvironment({ envIndex, updatedEnv }) {
|
||||
const backendId = environmentsStore.value.environments[envIndex].id
|
||||
|
||||
if (backendId) {
|
||||
updateUserEnvironment(backendId, updatedEnv)()
|
||||
}
|
||||
@@ -97,7 +106,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
||||
setGlobalVariables({ entries }) {
|
||||
const backendId = getGlobalVariableID()
|
||||
if (backendId) {
|
||||
updateUserEnvironment(backendId, { name: "", variables: entries })()
|
||||
updateUserEnvironment(backendId, {
|
||||
name: "",
|
||||
variables: entries,
|
||||
id: "",
|
||||
v: 1,
|
||||
})()
|
||||
}
|
||||
},
|
||||
clearGlobalVariables() {
|
||||
|
||||
Reference in New Issue
Block a user