chore: resolve global env in team env and tooltip UI update (#5187)

This commit is contained in:
Nivedin 2025-06-25 13:32:37 +05:30 committed by GitHub
parent 594f078b4e
commit 427a1811a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 303 additions and 135 deletions

View file

@ -158,7 +158,6 @@ a {
@apply shadow-none #{!important};
@apply fixed;
@apply inline-flex;
@apply -mt-7;
}
}
@ -169,7 +168,7 @@ a {
@apply shadow;
.tippy-content {
@apply flex;
@apply flex flex-col;
@apply text-tiny text-primary;
@apply font-semibold;
@apply px-2 py-1;

View file

@ -407,12 +407,14 @@
"added": "Environment addition",
"create_new": "Create new environment",
"created": "Environment created",
"current_value": "Current value",
"deleted": "Environment deletion",
"duplicated": "Environment duplicated",
"edit": "Edit Environment",
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"initial_value": "Initial value",
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",

View file

@ -239,9 +239,14 @@
{{ t("environment.name") }}
</span>
<span
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
class="min-w-[4rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.value") }}
{{ t("environment.initial_value") }}
</span>
<span
class="min-w-[4rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.current_value") }}
</span>
</div>
<div
@ -252,7 +257,13 @@
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<span class="min-w-[4rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.initialValue }}
</template>
</span>
<span class="min-w-[4rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.currentValue }}
@ -297,9 +308,14 @@
{{ t("environment.name") }}
</span>
<span
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
class="min-w-[4rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.value") }}
{{ t("environment.initial_value") }}
</span>
<span
class="min-w-[4rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.current_value") }}
</span>
</div>
<div
@ -310,7 +326,13 @@
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<span class="min-w-[4rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.initialValue }}
</template>
</span>
<span class="min-w-[4rem] w-full truncate text-secondaryLight">
<template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.currentValue }}

View file

@ -159,7 +159,11 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import {
Environment,
GlobalEnvironment,
parseTemplateStringE,
} from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { clone } from "lodash-es"
import { useToast } from "@composables/toast"
@ -181,6 +185,8 @@ import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { getEnvActionErrorMessage } from "~/helpers/error-messages"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { useReadonlyStream } from "~/composables/stream"
import { globalEnv$ } from "~/newstore/environments"
type EnvironmentVariable = {
id: number
@ -259,6 +265,8 @@ const vars = ref<EnvironmentVariable[]>([
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const secretVars = computed(() =>
pipe(
vars.value,
@ -302,6 +310,7 @@ const liveEnvs = computed(() => {
}
return [
...vars.value.map((x) => ({ ...x.env, sourceEnv: editingName.value! })),
...globalEnv.value.variables.map((x) => ({ ...x, sourceEnv: "Global" })),
]
})

View file

@ -17,7 +17,7 @@
v-else
ref="editor"
:placeholder="placeholder"
class="flex flex-1 truncate"
class="flex flex-1 truncate relative"
:class="styles"
@click="emit('click', $event)"
@keydown="handleKeystroke"
@ -85,7 +85,10 @@ import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { HoppPredefinedVariablesPlugin } from "~/helpers/editor/extensions/HoppPredefinedVariables"
import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import {
AggregateEnvironment,
aggregateEnvsWithCurrentValue$,
} from "~/newstore/environments"
import { platform } from "~/platform"
import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { InspectorResult } from "~/services/inspection"
@ -365,9 +368,10 @@ watch(
let clipboardEv: ClipboardEvent | null = null
let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const aggregateEnvs = useReadonlyStream(
aggregateEnvsWithCurrentValue$,
[]
) as Ref<AggregateEnvironment[]>
const tabs = useService(RESTTabService)
@ -377,7 +381,7 @@ const envVars = computed(() => {
const { key, secret } = x
const currentValue = secret ? "********" : x.currentValue
const initialValue = secret ? "********" : x.initialValue
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : ""
return {
key,
currentValue,

View file

@ -15,8 +15,8 @@ import { invokeAction } from "~/helpers/actions"
import { getService } from "~/modules/dioc"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
getAggregateEnvs,
aggregateEnvsWithCurrentValue$,
getAggregateEnvsWithCurrentValue,
getCurrentEnvironment,
getSelectedEnvironmentType,
} from "~/newstore/environments"
@ -25,6 +25,8 @@ import { RESTTabService } from "~/services/tab/rest"
import IconEdit from "~icons/lucide/edit?raw"
import IconUser from "~icons/lucide/user?raw"
import IconUsers from "~icons/lucide/users?raw"
import IconGlobe from "~icons/lucide/globe?raw"
import IconVariable from "~icons/lucide/variable?raw"
import { isComment } from "./helpers"
import { CurrentValueService } from "~/services/current-environment-value.service"
@ -121,49 +123,82 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
let envValue = "Not Found"
const envTooltipValue =
let envInitialValue = tooltipEnv?.initialValue
// If the environment is not a request variable, get the current value from the current environment service
let envCurrentValue =
tooltipEnv?.sourceEnv !== "RequestVariable"
? (currentEnvironmentValueService.getEnvironmentByKey(
? currentEnvironmentValueService.getEnvironmentByKey(
tooltipEnv?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipEnv?.key ?? ""
)?.currentValue ?? tooltipEnv?.currentValue)
)?.currentValue || tooltipEnv?.currentValue
: tooltipEnv?.currentValue
const hasSecretEnv = secretEnvironmentService.hasSecretValue(
const isSecret = tooltipEnv?.secret === true
const hasSource = Boolean(tooltipEnv?.sourceEnv)
const hasSecretStored = secretEnvironmentService.hasSecretValue(
tooltipEnv?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipEnv?.key ?? ""
)
if (!tooltipEnv?.secret && envTooltipValue) {
envValue = envTooltipValue
} else if (tooltipEnv?.secret && hasSecretEnv) {
envValue = "******"
} else if (tooltipEnv?.secret && !hasSecretEnv) {
envValue = "Empty"
} else if (!tooltipEnv?.sourceEnv) {
envValue = "Not Found"
} else if (!envTooltipValue) {
envValue = "Empty"
// We need to check if the environment is a secret and if it has a secret value stored in the secret environment service
// If it is a secret and has a secret value, we need to show "******" in the tooltip
// If it is a secret and does not have a secret value, we need to show "Empty" in the tooltip
// If it is not a secret, we need to show the current value or initial value
// If the environment is not found, we need to show "Not Found" in the tooltip
// If the source environment is not found, we need to show "Not Found" in the tooltip, ie the the environment
// is not defined in the selected environment or the global environment
if (isSecret) {
if (!hasSecretStored && envInitialValue) {
envInitialValue = "******"
} else if (hasSecretStored && !envInitialValue) {
envCurrentValue = "******"
} else if (hasSecretStored && envInitialValue) {
envInitialValue = "******"
envCurrentValue = "******"
} else {
envInitialValue = "Empty"
envCurrentValue = "Empty"
}
} else if (!hasSource) {
envInitialValue = "Not Found"
envCurrentValue = "Not Found"
} else {
// Parse templates only if needed and values are not already masked
if (!envCurrentValue && envInitialValue) {
const parsedInitial = parseTemplateStringE(
envInitialValue,
aggregateEnvs
)
envInitialValue = E.isLeft(parsedInitial)
? "error"
: parsedInitial.right
} else if (!envInitialValue && envCurrentValue) {
const parsedCurrent = parseTemplateStringE(
envCurrentValue,
aggregateEnvs
)
envCurrentValue = E.isLeft(parsedCurrent)
? "error"
: parsedCurrent.right
}
}
const result = parseTemplateStringE(envValue, aggregateEnvs)
let finalEnv = E.isLeft(result) ? "error" : result.right
// If the request variable has an secret variable
// parseTemplateStringE is passed the secret value which has value undefined
// So, we need to check if the result is undefined and then set the finalEnv to ******
if (finalEnv === "undefined") finalEnv = "******"
const selectedEnvType = getSelectedEnvironmentType()
// Set the icon based on the source environment
const envTypeIcon = `<span class="inline-flex items-center justify-center my-1">${
selectedEnvType === "TEAM_ENV" ? IconUsers : IconUser
tooltipEnv?.sourceEnv === "Global"
? IconGlobe
: tooltipEnv?.sourceEnv === "RequestVariable"
? IconVariable
: selectedEnvType === "TEAM_ENV"
? IconUsers
: IconUser
}</span>`
const appendEditAction = (tooltip: HTMLElement) => {
@ -205,22 +240,66 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
}
return {
pos: start,
end: to,
above: true,
// The start and end positions of the environment variable in the text
// We add 2 to the end position to include the closing `>>` in the tooltip
// and -1 to the start position to include the opening `<<` in the tooltip
pos: start - 1,
end: end + 2,
arrow: true,
create() {
const dom = document.createElement("span")
const tooltipContainer = document.createElement("span")
const kbd = document.createElement("kbd")
const dom = document.createElement("div")
const tooltipContainer = document.createElement("div")
const tooltipHeaderBlock = document.createElement("div")
tooltipHeaderBlock.className =
"flex items-center justify-between w-full space-x-2 "
tooltipContainer.appendChild(tooltipHeaderBlock)
const iconNameContainer = document.createElement("div")
iconNameContainer.className =
"flex items-center space-x-2 flex-1 mr-4 "
tooltipHeaderBlock.appendChild(iconNameContainer)
const icon = document.createElement("span")
icon.innerHTML = envTypeIcon
icon.className = "mr-2"
kbd.textContent = finalEnv
tooltipContainer.appendChild(icon)
tooltipContainer.appendChild(document.createTextNode(`${envName} `))
tooltipContainer.appendChild(kbd)
if (tooltipEnv) appendEditAction(tooltipContainer)
const envNameBlock = document.createElement("span")
envNameBlock.innerText = envName
iconNameContainer.appendChild(icon)
iconNameContainer.appendChild(envNameBlock)
if (tooltipEnv) appendEditAction(tooltipHeaderBlock)
const envContainer = document.createElement("div")
tooltipContainer.appendChild(envContainer)
envContainer.className =
"flex flex-col items-start space-y-1 flex-1 w-full mt-2"
const initialValueBlock = document.createElement("div")
initialValueBlock.className = "flex items-center space-x-2"
const initialValueTitle = document.createElement("div")
const initialValue = document.createElement("span")
initialValue.textContent = envInitialValue || ""
initialValueTitle.textContent = "Initial"
initialValueTitle.className = "font-bold mr-4 "
initialValueBlock.appendChild(initialValueTitle)
initialValueBlock.appendChild(initialValue)
const currentValueBlock = document.createElement("div")
currentValueBlock.className = "flex items-center space-x-2"
const currentValueTitle = document.createElement("div")
const currentValue = document.createElement("span")
currentValue.textContent = envCurrentValue || ""
currentValueTitle.textContent = "Current "
currentValueTitle.className = "font-bold mr-1.5"
currentValueBlock.appendChild(currentValueTitle)
currentValueBlock.appendChild(currentValue)
envContainer.appendChild(initialValueBlock)
envContainer.appendChild(currentValueBlock)
tooltipContainer.className = "tippy-content"
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
@ -294,7 +373,7 @@ export class HoppEnvironmentPlugin {
subscribeToStream: StreamSubscriberFunc,
private editorView: Ref<EditorView | undefined>
) {
const aggregateEnvs = getAggregateEnvs()
const aggregateEnvs = getAggregateEnvsWithCurrentValue()
const currentTab = restTabs.currentActiveTab.value
const currentTabRequest =
@ -302,6 +381,8 @@ export class HoppEnvironmentPlugin {
? currentTab.document.response.originalRequest
: currentTab.document.request
if (!currentTabRequest) return
watch(
currentTabRequest,
(request) => {
@ -332,7 +413,7 @@ export class HoppEnvironmentPlugin {
const requestVariables = currentTabRequest?.requestVariables ?? []
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
subscribeToStream(aggregateEnvsWithCurrentValue$, (envs) => {
this.envs = [
...requestVariables.map(({ key, value }) => ({
key,

View file

@ -80,27 +80,58 @@ const cursorTooltipField = () =>
: `${variableName} is not a valid predefined variable.`
return {
pos: start,
end: to,
above: true,
// The start and end positions of the environment variable in the text
// We add 2 to the end position to include the closing `>>` in the tooltip
// and -1 to the start position to include the opening `<<` in the tooltip
pos: start - 1,
end: end + 2,
arrow: true,
create() {
const dom = document.createElement("div")
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
const tooltipContainer = document.createElement("div")
const tooltipHeaderBlock = document.createElement("div")
tooltipHeaderBlock.className =
"flex items-center justify-between w-full space-x-2 "
tooltipContainer.appendChild(tooltipHeaderBlock)
const iconNameContainer = document.createElement("div")
iconNameContainer.className =
"flex items-center space-x-2 flex-1 mr-4 "
tooltipHeaderBlock.appendChild(iconNameContainer)
const icon = document.createElement("span")
icon.innerHTML = variableIcon
icon.className = "mr-2"
const tooltipContainer = document.createElement("span")
const envNameBlock = document.createElement("span")
envNameBlock.innerText = variableName
iconNameContainer.appendChild(icon)
iconNameContainer.appendChild(envNameBlock)
const envContainer = document.createElement("div")
tooltipContainer.appendChild(envContainer)
envContainer.className =
"flex flex-col items-start space-y-1 flex-1 w-full mt-2"
const valueBlock = document.createElement("div")
valueBlock.className = "flex items-center space-x-2"
const valueTitle = document.createElement("div")
const value = document.createElement("span")
value.textContent = variableDescription || ""
valueTitle.textContent = "Value"
valueTitle.className = "font-bold mr-4 "
valueBlock.appendChild(valueTitle)
valueBlock.appendChild(value)
envContainer.appendChild(valueBlock)
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
tooltipContainer.className = "tippy-content"
tooltipContainer.appendChild(icon)
tooltipContainer.appendChild(
document.createTextNode(variableDescription)
)
dom.appendChild(tooltipContainer)
return { dom }
},

View file

@ -12,6 +12,7 @@ import { getService } from "~/modules/dioc"
import DispatchingStore, {
defineDispatchers,
} from "~/newstore/DispatchingStore"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
export type SelectedEnvironmentIndex =
@ -49,6 +50,7 @@ const defaultEnvironmentsState = {
}
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
type EnvironmentStore = typeof defaultEnvironmentsState
@ -525,7 +527,7 @@ export function getAggregateEnvs() {
]
}
export function getAggregateEnvsWithSecrets() {
export function getAggregateEnvsWithCurrentValue() {
const currentEnv = getCurrentEnvironment()
return [
@ -541,7 +543,11 @@ export function getAggregateEnvsWithSecrets() {
return <AggregateEnvironment>{
key: x.key,
currentValue,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
currentEnv.id,
index
) ?? currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: currentEnv.name,
@ -558,7 +564,11 @@ export function getAggregateEnvsWithSecrets() {
}
return <AggregateEnvironment>{
key: x.key,
currentValue,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
"Global",
index
) ?? currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: "Global",
@ -567,51 +577,71 @@ export function getAggregateEnvsWithSecrets() {
]
}
export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
combineLatest([currentEnvironment$, globalEnv$]).pipe(
map(([selectedEnv, globalEnv]) => {
const results: AggregateEnvironment[] = []
export const aggregateEnvsWithCurrentValue$: Observable<
AggregateEnvironment[]
> = combineLatest([currentEnvironment$, globalEnv$]).pipe(
map(([selectedEnv, globalEnv]) => {
const results: AggregateEnvironment[] = []
selectedEnv?.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
selectedEnv.id,
index
) ?? ""
}
results.push({
key: x.key,
currentValue: currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: selectedEnv.name,
})
// Ensure pre-defined variables are prioritised over other environment variables with the same name
HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => {
results.push({
key,
currentValue: getValue(),
initialValue: getValue(),
secret: false,
sourceEnv: selectedEnv?.name ?? "Global",
})
})
globalEnv.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
) ?? ""
}
results.push({
key: x.key,
currentValue: currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: "Global",
})
selectedEnv?.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
selectedEnv.id,
index
) ?? ""
}
results.push({
key: x.key,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
selectedEnv.id,
index
) ?? currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: selectedEnv.name,
})
})
return results
}),
distinctUntilChanged(isEqual)
)
globalEnv.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
currentValue =
secretEnvironmentService.getSecretEnvironmentVariableValue(
"Global",
index
) ?? ""
}
results.push({
key: x.key,
currentValue:
currentEnvironmentValueService.getEnvironmentVariableValue(
"Global",
index
) ?? currentValue,
initialValue: x.initialValue,
secret: x.secret,
sourceEnv: "Global",
})
})
return results
}),
distinctUntilChanged(isEqual)
)
export function getCurrentEnvironment(): Environment {
if (

View file

@ -16,7 +16,7 @@ vi.mock("~/newstore/environments", async () => {
return {
__esModule: true,
aggregateEnvsWithSecrets$: new BehaviorSubject([
aggregateEnvsWithCurrentValue$: new BehaviorSubject([
{
key: "EXISTING_ENV_VAR",
currentValue: "test_value",

View file

@ -14,7 +14,7 @@ import {
} from "@hoppscotch/data"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
aggregateEnvsWithCurrentValue$,
getCurrentEnvironment,
getSelectedEnvironmentType,
} from "~/newstore/environments"
@ -50,8 +50,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
private readonly currentEnvs = this.bind(CurrentValueService)
private readonly restTabs = this.bind(RESTTabService)
private aggregateEnvsWithSecrets = useStreamStatic(
aggregateEnvsWithSecrets$,
private aggregateEnvsWithValue = useStreamStatic(
aggregateEnvsWithCurrentValue$,
[],
() => {
/* noop */
@ -85,7 +85,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const environmentVariables = [
...(currentTabRequest?.requestVariables ?? []),
...this.aggregateEnvsWithSecrets.value,
...this.aggregateEnvsWithValue.value,
]
const envKeys = environmentVariables.map((e) => e.key)
@ -171,7 +171,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
/**
* Checks if the environment variables in the target array are empty
* Checks if the environment variables in the target have empty current value or initial value.
* @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
@ -210,7 +210,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
secret: false,
sourceEnv: "RequestVariable",
})),
...this.aggregateEnvsWithSecrets.value,
...this.aggregateEnvsWithValue.value,
])
environmentVariables.forEach((env) => {
@ -221,16 +221,18 @@ export class EnvironmentInspectorService extends Service implements Inspector {
env.key
)
const hasCurrentValue =
const hasValue =
this.currentEnvs.hasValue(
env.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
env.key
) || env.currentValue !== ""
) ||
env.currentValue !== "" ||
env.initialValue !== ""
if (env.key === formattedExEnv) {
if (env.secret ? !hasSecretEnv : !hasCurrentValue) {
if (env.secret ? !hasSecretEnv : !hasValue) {
const itemLocation: InspectorLocation = {
type: locations.type,
position:

View file

@ -102,13 +102,7 @@ export const parseBodyEnvVariables = (
export function parseTemplateStringE(
str: string,
variables:
| Environment["variables"]
| {
secret: true
currentValue: string
key: string
}[],
variables: Environment["variables"],
maskValue = false,
showKeyIfSecret = false
) {
@ -183,13 +177,7 @@ export type NonSecretEnvironment = Omit<Environment, "variables"> & {
*/
export const parseTemplateString = (
str: string,
variables:
| Environment["variables"]
| {
secret: true
currentValue: string
key: string
}[],
variables: Environment["variables"],
maskValue = false,
showKeyIfSecret = false
) =>