Skip to content

Commit

Permalink
add variables ui
Browse files Browse the repository at this point in the history
  • Loading branch information
dvjn committed Aug 3, 2024
1 parent 42f2734 commit 6954073
Show file tree
Hide file tree
Showing 14 changed files with 616 additions and 4 deletions.
5 changes: 5 additions & 0 deletions web/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare module 'vue' {
AdminReposTab: typeof import('./src/components/admin/settings/AdminReposTab.vue')['default']
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
AdminVariablesTab: typeof import('./src/components/admin/settings/AdminVariablesTab.vue')['default']
Badge: typeof import('./src/components/atomic/Badge.vue')['default']
BadgeTab: typeof import('./src/components/repo/settings/BadgeTab.vue')['default']
Button: typeof import('./src/components/atomic/Button.vue')['default']
Expand Down Expand Up @@ -112,6 +113,10 @@ declare module 'vue' {
UserCLIAndAPITab: typeof import('./src/components/user/UserCLIAndAPITab.vue')['default']
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default']
UserVariablesTab: typeof import('./src/components/user/UserVariablesTab.vue')['default']
VariableEdit: typeof import('./src/components/variables/VariableEdit.vue')['default']
VariableList: typeof import('./src/components/variables/VariableList.vue')['default']
VariablesTab: typeof import('./src/components/repo/settings/VariablesTab.vue')['default']
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
}
}
28 changes: 28 additions & 0 deletions web/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@
"not_allowed": "You are not allowed to access this organization's settings",
"secrets": {
"desc": "Organization secrets can be passed to all organization's repository individual pipeline steps at runtime as environmental variables."
},
"variables": {
"desc": "Organization variables can be passed to all organization's repository individual pipeline steps at runtime as environmental variables."
}
}
},
Expand All @@ -285,6 +288,10 @@
"desc": "Global secrets can be passed to all repositories individual pipeline steps at runtime as environmental variables.",
"warning": "These secrets will be available for all server users."
},
"variables": {
"desc": "Global variables can be passed to all repositories individual pipeline steps at runtime as environmental variables.",
"warning": "These variables will be available for all server users."
},
"agents": {
"agents": "Agents",
"desc": "Agents registered for this server",
Expand Down Expand Up @@ -406,8 +413,13 @@
}
},
"secrets": {
"secrets": "Secrets",
"desc": "User secrets can be passed to all user's repository individual pipeline steps at runtime as environmental variables."
},
"variables": {
"variables": "Variables",
"desc": "User variables can be passed to all user's repository individual pipeline steps at runtime as environmental variables."
},
"cli_and_api": {
"cli_and_api": "CLI & API",
"desc": "Personal Access Token, CLI and API usage",
Expand All @@ -423,6 +435,22 @@
"internal_error": "Some internal error occurred",
"access_denied": "You are not allowed to login"
},
"variables": {
"variables": "Variables",
"desc": "Variables can be passed to individual pipeline steps at runtime as environmental variables.",
"none": "There are no variables yet.",
"add": "Add variable",
"save": "Save variable",
"show": "Show variables",
"name": "Name",
"value": "Value",
"delete_confirm": "Do you really want to delete this variable?",
"deleted": "Variable deleted",
"created": "Variable created",
"saved": "Variable saved",
"edit": "Edit variable",
"delete": "Delete variable"
},
"secrets": {
"secrets": "Secrets",
"desc": "Secrets can be passed to individual pipeline steps at runtime as environmental variables.",
Expand Down
100 changes: 100 additions & 0 deletions web/src/components/admin/settings/AdminVariablesTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<template>
<Settings
:title="$t('variables.variables')"
:desc="$t('admin.settings.variables.desc')"
docs-url="docs/usage/variables"
:warning="$t('admin.settings.variables.warning')"
>
<template #titleActions>
<Button
v-if="selectedVariable"
:text="$t('variables.show')"
start-icon="back"
@click="selectedVariable = undefined"
/>
<Button v-else :text="$t('variables.add')" start-icon="plus" @click="showAddVariable" />
</template>

<VariableList
v-if="!selectedVariable"
v-model="variables"
:is-deleting="isDeleting"
@edit="editVariable"
@delete="deleteVariable"
/>

<VariableEdit
v-else
v-model="selectedVariable"
:is-saving="isSaving"
@save="createVariable"
@cancel="selectedVariable = undefined"
/>
</Settings>
</template>

<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import VariableEdit from '~/components/variables/VariableEdit.vue';
import VariableList from '~/components/variables/VariableList.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import { Variable } from '~/lib/api/types';
const emptyVariable: Partial<Variable> = {
name: '',
value: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
const selectedVariable = ref<Partial<Variable>>();
const isEditingVariable = computed(() => !!selectedVariable.value?.id);
async function loadVariables(page: number): Promise<Variable[] | null> {
return apiClient.getGlobalSecretList({ page });
}
const { resetPage, data: variables } = usePagination(loadVariables, () => !selectedVariable.value);
const { doSubmit: createVariable, isLoading: isSaving } = useAsyncAction(async () => {
if (!selectedVariable.value) {
throw new Error("Unexpected: Can't get variable");
}
if (isEditingVariable.value) {
await apiClient.updateGlobalSecret(selectedVariable.value);
} else {
await apiClient.createGlobalSecret(selectedVariable.value);
}
notifications.notify({
title: i18n.t(isEditingVariable.value ? 'variables.saved' : 'variables.created'),
type: 'success',
});
selectedVariable.value = undefined;
resetPage();
});
const { doSubmit: deleteVariable, isLoading: isDeleting } = useAsyncAction(async (_variable: Variable) => {
await apiClient.deleteGlobalSecret(_variable.name);
notifications.notify({ title: i18n.t('variables.deleted'), type: 'success' });
resetPage();
});
function editVariable(variable: Variable) {
selectedVariable.value = cloneDeep(variable);
}
function showAddVariable() {
selectedVariable.value = cloneDeep(emptyVariable);
}
</script>
148 changes: 148 additions & 0 deletions web/src/components/repo/settings/VariablesTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template>
<Settings :title="$t('variables.variables')" :desc="$t('variables.desc')" docs-url="docs/usage/variables">
<template #titleActions>
<Button
v-if="selectedVariable"
:text="$t('variables.show')"
start-icon="back"
@click="selectedVariable = undefined"
/>
<Button v-else :text="$t('variables.add')" start-icon="plus" @click="showAddVariable" />
</template>

<VariableList
v-if="!selectedVariable"
:model-value="variables"
:is-deleting="isDeleting"
@edit="editVariable"
@delete="deleteSecret"
/>

<VariableEdit
v-else
v-model="selectedVariable"
:is-saving="isSaving"
@save="createSecret"
@cancel="selectedVariable = undefined"
/>
</Settings>
</template>

<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, inject, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import VariableEdit from '~/components/variables/VariableEdit.vue';
import VariableList from '~/components/variables/VariableList.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import { Repo, Variable } from '~/lib/api/types';
const emptyVariable: Partial<Variable> = {
name: '',
value: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
const repo = inject<Ref<Repo>>('repo');
const selectedVariable = ref<Partial<Variable>>();
const isEditingVariable = computed(() => !!selectedVariable.value?.id);
async function loadVariables(page: number, level: 'repo' | 'org' | 'global'): Promise<Variable[] | null> {
if (!repo?.value) {
throw new Error("Unexpected: Can't load repo");
}
switch (level) {
case 'repo':
return apiClient.getSecretList(repo.value.id, { page });
case 'org':
return apiClient.getOrgSecretList(repo.value.org_id, { page });
case 'global':
return apiClient.getGlobalSecretList({ page });
default:
throw new Error(`Unexpected level: ${level}`);
}
}
const { resetPage, data: _variables } = usePagination(loadVariables, () => !selectedVariable.value, {
each: ['repo', 'org', 'global'],
pageSize: 50,
});
const variables = computed(() => {
const variablesList: Record<string, Variable & { edit?: boolean; level: 'repo' | 'org' | 'global' }> = {};
// eslint-disable-next-line no-restricted-syntax
for (const level of ['repo', 'org', 'global']) {
// eslint-disable-next-line no-restricted-syntax
for (const variable of _variables.value) {
if (
((level === 'repo' && variable.repo_id !== 0 && variable.org_id === 0) ||
(level === 'org' && variable.repo_id === 0 && variable.org_id !== 0) ||
(level === 'global' && variable.repo_id === 0 && variable.org_id === 0)) &&
!variablesList[variable.name]
) {
variablesList[variable.name] = { ...variable, edit: variable.repo_id !== 0, level };
}
}
}
const levelsOrder = {
global: 0,
org: 1,
repo: 2,
};
return Object.values(variablesList)
.toSorted((a, b) => a.name.localeCompare(b.name))
.toSorted((a, b) => levelsOrder[b.level] - levelsOrder[a.level]);
});
const { doSubmit: createSecret, isLoading: isSaving } = useAsyncAction(async () => {
if (!repo?.value) {
throw new Error("Unexpected: Can't load repo");
}
if (!selectedVariable.value) {
throw new Error("Unexpected: Can't get variable");
}
if (isEditingVariable.value) {
await apiClient.updateSecret(repo.value.id, selectedVariable.value);
} else {
await apiClient.createSecret(repo.value.id, selectedVariable.value);
}
notifications.notify({
title: i18n.t(isEditingVariable.value ? 'variables.saved' : 'variables.created'),
type: 'success',
});
selectedVariable.value = undefined;
await resetPage();
});
const { doSubmit: deleteSecret, isLoading: isDeleting } = useAsyncAction(async (_variable: Variable) => {
if (!repo?.value) {
throw new Error("Unexpected: Can't load repo");
}
await apiClient.deleteSecret(repo.value.id, _variable.name);
notifications.notify({ title: i18n.t('variables.deleted'), type: 'success' });
await resetPage();
});
function editVariable(variable: Variable) {
selectedVariable.value = cloneDeep(variable);
}
function showAddVariable() {
selectedVariable.value = cloneDeep(emptyVariable);
}
</script>
6 changes: 2 additions & 4 deletions web/src/components/user/UserSecretsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@
<Button
v-if="selectedSecret"
class="ml-auto"
:text="$t('user.settings.secrets.show')"
:text="$t('secrets.show')"
start-icon="back"
@click="selectedSecret = undefined"
/>
<Button v-else class="ml-auto" :text="$t('user.settings.secrets.add')" start-icon="plus" @click="showAddSecret" />
<Button v-else class="ml-auto" :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</div>

<SecretList
v-if="!selectedSecret"
v-model="secrets"
i18n-prefix="user.settings.secrets."
:is-deleting="isDeleting"
@edit="editSecret"
@delete="deleteSecret"
Expand All @@ -30,7 +29,6 @@
<SecretEdit
v-else
v-model="selectedSecret"
i18n-prefix="user.settings.secrets."
:is-saving="isSaving"
@save="createSecret"
@cancel="selectedSecret = undefined"
Expand Down
Loading

0 comments on commit 6954073

Please sign in to comment.