refactor: make global environment a versioned entity (#4216)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Palak Chopra
2024-09-23 17:01:14 +05:30
committed by GitHub
parent 1701961335
commit bfe3b3a9c3
17 changed files with 337 additions and 138 deletions

View File

@@ -205,7 +205,7 @@
</span>
</div>
<div
v-for="(variable, index) in globalEnvs"
v-for="(variable, index) in globalEnvs.variables"
:key="index"
class="flex flex-1 space-x-4"
>
@@ -219,7 +219,10 @@
</template>
</span>
</div>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
<div
v-if="globalEnvs.variables.length === 0"
class="text-secondaryLight"
>
{{ t("environment.empty_variables") }}
</div>
</div>
@@ -292,37 +295,36 @@
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import IconEye from "~icons/lucide/eye"
import IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe"
import { useColorMode } from "@composables/theming"
import { Environment, GlobalEnvironment } from "@hoppscotch/data"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { useService } from "dioc/vue"
import { computed, onMounted, ref, watch } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
import { useReadonlyStream, useStream } from "~/composables/stream"
import { invokeAction } from "~/helpers/actions"
import { GQLError } from "~/helpers/backend/GQLClient"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import {
environments$,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
import { onMounted } from "vue"
import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import {
sortPersonalEnvironmentsAlphabetically,
sortTeamEnvironmentsAlphabetically,
} from "~/helpers/utils/sortEnvironmentsAlphabetically"
import IconCheck from "~icons/lucide/check"
import IconEdit from "~icons/lucide/edit"
import IconEye from "~icons/lucide/eye"
import IconGlobe from "~icons/lucide/globe"
import IconLayers from "~icons/lucide/layers"
type Scope =
| {
@@ -600,7 +602,7 @@ const getErrorMessage = (err: GQLError<string>) => {
}
}
const globalEnvs = useReadonlyStream(globalEnv$, [])
const globalEnvs = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {

View File

@@ -48,7 +48,7 @@
<script setup lang="ts">
import { useReadonlyStream, useStream } from "@composables/stream"
import { Environment } from "@hoppscotch/data"
import { Environment, GlobalEnvironment } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
@@ -86,13 +86,13 @@ const environmentType = ref<EnvironmentsChooseType>({
selectedTeam: undefined,
})
const globalEnv = useReadonlyStream(globalEnv$, [])
const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const globalEnvironment = computed(() => ({
const globalEnvironment = computed<GlobalEnvironment>(() => ({
v: 1 as const,
id: "Global",
name: "Global",
variables: globalEnv.value,
variables: globalEnv.value.variables,
}))
const isPersonalEnvironmentType = computed(
@@ -252,7 +252,7 @@ defineActionHandler("modals.environment.delete-selected", () => {
const additionalVars = ref<Environment["variables"]>([])
const envVars = () => [...globalEnv.value, ...additionalVars.value]
const envVars = () => [...globalEnv.value.variables, ...additionalVars.value]
defineActionHandler(
"modals.global.environment.update",

View File

@@ -133,21 +133,27 @@
</template>
<script setup lang="ts">
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 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"
import { pipe, flow } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
import {
Environment,
GlobalEnvironment,
parseTemplateStringE,
} from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useService } from "dioc/vue"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import { flow, pipe } from "fp-ts/function"
import { ComputedRef, computed, ref, watch } from "vue"
import { uniqueID } from "~/helpers/utils/uniqueID"
import {
createEnvironment,
environments$,
environmentsStore,
getEnvironment,
getGlobalVariables,
globalEnv$,
@@ -155,15 +161,13 @@ import {
setSelectedEnvironmentIndex,
updateEnvironment,
} from "~/newstore/environments"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
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 "~/helpers/utils/uniqueID"
import IconDone from "~icons/lucide/check"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconPlus from "~icons/lucide/plus"
import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2"
type EnvironmentVariable = {
id: number
@@ -257,7 +261,7 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
1000
)
const globalVars = useReadonlyStream(globalEnv$, [])
const globalVars = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
type SelectedEnv = "variables" | "secret"
@@ -315,10 +319,20 @@ const liveEnvs = computed(() => {
}
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
...globalVars.value.variables.map((x) => ({ ...x, source: "Global" })),
]
})
const workingEnvID = computed(() => {
const activeEnv = workingEnv.value
if (activeEnv && "id" in activeEnv) {
return activeEnv.id
}
return uniqueID()
})
watch(
() => props.show,
(show) => {
@@ -329,7 +343,7 @@ watch(
: "variables"
if (props.editingEnvironmentIndex !== "Global") {
editingID.value = workingEnv.value?.id || uniqueID()
editingID.value = workingEnvID.value
}
vars.value = pipe(
workingEnv.value?.variables ?? [],
@@ -341,7 +355,7 @@ watch(
? secretEnvironmentService.getSecretEnvironmentVariable(
props.editingEnvironmentIndex === "Global"
? "Global"
: workingEnv.value?.id,
: workingEnvID.value,
index
)?.value ?? ""
: e.value,
@@ -448,7 +462,7 @@ const saveEnvironment = () => {
})
} else if (props.editingEnvironmentIndex === "Global") {
// Editing the Global environment
setGlobalEnvVariables(environmentUpdated.variables)
setGlobalEnvVariables(environmentUpdated)
toast.success(`${t("environment.updated")}`)
} else if (props.editingEnvironmentIndex !== null) {
const envID =

View File

@@ -204,27 +204,28 @@
</template>
<script setup lang="ts">
import { computed, Ref, ref } from "vue"
import { isEqual } from "lodash-es"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream, useStream } from "@composables/stream"
import { isEqual } from "lodash-es"
import { computed, ref } from "vue"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import {
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import IconCheck from "~icons/lucide/check"
import IconExternalLink from "~icons/lucide/external-link"
import IconTrash2 from "~icons/lucide/trash-2"
import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming"
import { GlobalEnvironment } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import { useColorMode } from "~/composables/theming"
import { invokeAction } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
const props = defineProps<{
modelValue: HoppTestResult | null | undefined
@@ -282,12 +283,7 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex
)
const globalEnvVars = useReadonlyStream(globalEnv$, []) as Ref<
Array<{
key: string
value: string
}>
>
const globalEnvVars = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const noEnvSelected = computed(
() => selectedEnvironmentIndex.value.type === "NO_ENV_SELECTED"
@@ -297,7 +293,8 @@ const globalHasAdditions = computed(() => {
if (!testResults.value?.envDiff.selected.additions) return false
return (
testResults.value.envDiff.selected.additions.every(
(x) => globalEnvVars.value.findIndex((y) => isEqual(x, y)) !== -1
(x) =>
globalEnvVars.value.variables.findIndex((y) => isEqual(x, y)) !== -1
) ?? false
)
})

View File

@@ -303,12 +303,15 @@ export function runRESTRequest$(
tab.value.document.testResults =
translateToSandboxTestResults(updatedRunResult)
setGlobalEnvVariables(
updateEnvironmentsWithSecret(
runResult.right.envs.global,
"global"
)
const globalEnvVariables = updateEnvironmentsWithSecret(
runResult.right.envs.global,
"global"
)
setGlobalEnvVariables({
v: 1,
variables: globalEnvVariables,
})
if (
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
) {

View File

@@ -1,4 +1,8 @@
import { Environment } from "@hoppscotch/data"
import {
Environment,
GlobalEnvironment,
GlobalEnvironmentVariable,
} from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es"
import { combineLatest, Observable } from "rxjs"
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
@@ -19,6 +23,11 @@ export type SelectedEnvironmentIndex =
environment: Environment
}
const defaultGlobalEnvironmentState: GlobalEnvironment = {
v: 1,
variables: [],
}
const defaultEnvironmentsState = {
environments: [
{
@@ -31,7 +40,7 @@ const defaultEnvironmentsState = {
// as a temp fix for identifying global env when syncing
globalEnvID: undefined as string | undefined,
globals: [] as Environment["variables"],
globals: defaultGlobalEnvironmentState,
selectedEnvironmentIndex: {
type: "NO_ENV_SELECTED",
@@ -277,7 +286,7 @@ const dispatchers = defineDispatchers({
),
}
},
setGlobalVariables(_, { entries }: { entries: Environment["variables"] }) {
setGlobalVariables(_, { entries }: { entries: GlobalEnvironment }) {
return {
globals: entries,
}
@@ -285,20 +294,26 @@ const dispatchers = defineDispatchers({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clearGlobalVariables(_store, {}) {
return {
globals: [],
globals: defaultGlobalEnvironmentState,
}
},
addGlobalVariable(
{ globals },
{ entry }: { entry: Environment["variables"][number] }
{ entry }: { entry: GlobalEnvironmentVariable }
) {
return {
globals: [...globals, entry],
globals: {
...globals,
variables: [...globals.variables, entry],
},
}
},
removeGlobalVariable({ globals }, { envIndex }: { envIndex: number }) {
return {
globals: globals.filter((_, i) => i !== envIndex),
globals: {
...globals,
variables: globals.variables.filter((_, i) => i !== envIndex),
},
}
},
updateGlobalVariable(
@@ -306,10 +321,15 @@ const dispatchers = defineDispatchers({
{
envIndex,
updatedEntry,
}: { envIndex: number; updatedEntry: Environment["variables"][number] }
}: { envIndex: number; updatedEntry: GlobalEnvironmentVariable }
) {
return {
globals: globals.map((x, i) => (i !== envIndex ? x : updatedEntry)),
globals: {
...globals,
variables: globals.variables.map((x, i) =>
i !== envIndex ? x : updatedEntry
),
},
}
},
setGlobalEnvID(_, { id }: { id: string }) {
@@ -390,12 +410,19 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
map(([selectedEnv, globalVars]) => {
const results: AggregateEnvironment[] = []
selectedEnv?.variables.forEach(({ key, value, secret }) =>
selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
results.push({ key, value, secret, sourceEnv: selectedEnv.name })
)
globalVars.forEach(({ key, value, secret }) =>
})
globalVars.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
results.push({ key, value, secret, sourceEnv: "Global" })
)
})
return results
}),
@@ -496,7 +523,7 @@ export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
})
})
globalVars.map((x, index) => {
globalVars.variables.map((x, index) => {
let value
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(
@@ -568,19 +595,19 @@ export function getLegacyGlobalEnvironment(): Environment | null {
return el ?? null
}
export function getGlobalVariables(): Environment["variables"] {
return environmentsStore.value.globals.map(
(env: Environment["variables"][number]) => {
export function getGlobalVariables(): GlobalEnvironmentVariable[] {
return environmentsStore.value.globals.variables.map(
(env: GlobalEnvironmentVariable) => {
if (env.key && "value" in env && !("secret" in env)) {
return {
...(env as Environment["variables"][number]),
...(env as GlobalEnvironmentVariable),
secret: false,
}
}
return env
}
) as Environment["variables"]
) as GlobalEnvironmentVariable[]
}
export function getGlobalVariableID() {
@@ -595,7 +622,7 @@ export function getLocalIndexByEnvironmentID(id: string) {
return envIndex !== -1 ? envIndex : null
}
export function addGlobalEnvVariable(entry: Environment["variables"][number]) {
export function addGlobalEnvVariable(entry: GlobalEnvironmentVariable) {
environmentsStore.dispatch({
dispatcher: "addGlobalVariable",
payload: {
@@ -604,7 +631,7 @@ export function addGlobalEnvVariable(entry: Environment["variables"][number]) {
})
}
export function setGlobalEnvVariables(entries: Environment["variables"]) {
export function setGlobalEnvVariables(entries: GlobalEnvironment) {
environmentsStore.dispatch({
dispatcher: "setGlobalVariables",
payload: {
@@ -631,7 +658,7 @@ export function removeGlobalEnvVariable(envIndex: number) {
export function updateGlobalEnvVariable(
envIndex: number,
updatedEntry: Environment["variables"][number]
updatedEntry: GlobalEnvironmentVariable
) {
environmentsStore.dispatch({
dispatcher: "updateGlobalVariable",

View File

@@ -1,5 +1,6 @@
import {
Environment,
GlobalEnvironment,
HoppCollection,
RESTReqSchemaVersion,
} from "@hoppscotch/data"
@@ -115,9 +116,10 @@ export const MQTT_REQUEST_MOCK = {
clientID: "hoppscotch",
}
export const GLOBAL_ENV_MOCK: Environment["variables"] = [
{ key: "test-key", value: "test-value", secret: false },
]
export const GLOBAL_ENV_MOCK: GlobalEnvironment = {
v: 1,
variables: [{ key: "test-key", value: "test-value", secret: false }],
}
export const VUEX_DATA_MOCK: VUEX_DATA = {
postwoman: {

View File

@@ -1469,9 +1469,9 @@ describe("PersistenceService", () => {
// Invalid shape for `globalEnv`
const globalEnv = [
{
...GLOBAL_ENV_MOCK[0],
// `key` -> `string`
key: 1,
value: "test-value",
},
]

View File

@@ -2,6 +2,8 @@
import {
Environment,
GlobalEnvironment,
GlobalEnvironmentVariable,
translateToNewGQLCollection,
translateToNewRESTCollection,
} from "@hoppscotch/data"
@@ -51,9 +53,9 @@ import {
performSettingsDataMigrations,
settingsStore,
} from "../../newstore/settings"
import { SecretEnvironmentService } from "../secret-environment.service"
import {
ENVIRONMENTS_SCHEMA,
GLOBAL_ENV_SCHEMA,
GQL_COLLECTION_SCHEMA,
GQL_HISTORY_ENTRY_SCHEMA,
GQL_TAB_STATE_SCHEMA,
@@ -63,16 +65,15 @@ import {
REST_COLLECTION_SCHEMA,
REST_HISTORY_ENTRY_SCHEMA,
REST_TAB_STATE_SCHEMA,
SECRET_ENVIRONMENT_VARIABLE_SCHEMA,
SELECTED_ENV_INDEX_SCHEMA,
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
@@ -421,9 +422,8 @@ export class PersistenceService extends Service {
if (globalIndex !== -1) {
const globalEnv = environmentsData[globalIndex]
globalEnv.variables.forEach(
(variable: Environment["variables"][number]) =>
addGlobalEnvVariable(variable)
globalEnv.variables.forEach((variable: GlobalEnvironmentVariable) =>
addGlobalEnvVariable(variable)
)
// Remove global from environments
@@ -627,14 +627,14 @@ export class PersistenceService extends Service {
private setupGlobalEnvsPersistence() {
const globalEnvKey = "globalEnv"
let globalEnvData: z.infer<typeof GLOBAL_ENV_SCHEMA> = JSON.parse(
let globalEnvData: GlobalEnvironment = JSON.parse(
window.localStorage.getItem(globalEnvKey) || "[]"
)
// Validate data read from localStorage
const result = GLOBAL_ENV_SCHEMA.safeParse(globalEnvData)
if (result.success) {
globalEnvData = result.data
const result = GlobalEnvironment.safeParse(globalEnvData)
if (result.type === "ok") {
globalEnvData = result.value
} else {
this.showErrorToast(globalEnvKey)
window.localStorage.setItem(
@@ -643,7 +643,7 @@ export class PersistenceService extends Service {
)
}
setGlobalEnvVariables(globalEnvData as Environment["variables"])
setGlobalEnvVariables(globalEnvData)
globalEnv$.subscribe((vars) => {
window.localStorage.setItem(globalEnvKey, JSON.stringify(vars))

View File

@@ -249,8 +249,6 @@ const EnvironmentVariablesSchema = z.union([
}),
])
export const GLOBAL_ENV_SCHEMA = z.array(EnvironmentVariablesSchema)
const OperationTypeSchema = z.enum([
"subscription",
"query",