feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user