mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-22 14:16:18 +00:00
Add editing of secrets and registries (#823)
This commit is contained in:
parent
da99f47553
commit
2f6f44417d
8 changed files with 93 additions and 36 deletions
|
@ -25,6 +25,7 @@
|
|||
"fuse.js": "6.4.6",
|
||||
"humanize-duration": "3.27.0",
|
||||
"javascript-time-ago": "2.3.10",
|
||||
"lodash": "4.17.21",
|
||||
"node-emoji": "1.11.0",
|
||||
"pinia": "2.0.0",
|
||||
"vue": "v3.2.20",
|
||||
|
@ -34,6 +35,7 @@
|
|||
"@iconify/json": "1.1.421",
|
||||
"@types/humanize-duration": "3.27.0",
|
||||
"@types/javascript-time-ago": "2.0.3",
|
||||
"@types/lodash": "4.14.179",
|
||||
"@types/node": "16.11.6",
|
||||
"@types/node-emoji": "1.8.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.6.0",
|
||||
|
|
|
@ -32,9 +32,9 @@
|
|||
@click="doClick"
|
||||
>
|
||||
<slot>
|
||||
<Icon v-if="startIcon" :name="startIcon" class="mr-1" :class="{ invisible: isLoading }" />
|
||||
<Icon v-if="startIcon" :name="startIcon" class="mr-1 !w-6 !h-6" :class="{ invisible: isLoading }" />
|
||||
<span :class="{ invisible: isLoading }">{{ text }}</span>
|
||||
<Icon v-if="endIcon" :name="endIcon" class="ml-2" :class="{ invisible: isLoading }" />
|
||||
<Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" />
|
||||
<div
|
||||
class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center"
|
||||
:class="{
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<i-bx-bx-power-off v-else-if="name === 'turn-off'" class="h-6 w-6" />
|
||||
<i-mdi-chevron-right v-else-if="name === 'chevron-right'" class="h-6 w-6" />
|
||||
<i-carbon-close-outline v-else-if="name === 'close'" class="h-6 w-6" />
|
||||
<i-ic-baseline-edit v-else-if="name === 'edit'" class="h-6 w-6" />
|
||||
<div v-else-if="name === 'blank'" class="h-6 w-6" />
|
||||
</template>
|
||||
|
||||
|
@ -74,7 +75,8 @@ export type IconNames =
|
|||
| 'heal'
|
||||
| 'chevron-right'
|
||||
| 'turn-off'
|
||||
| 'close';
|
||||
| 'close'
|
||||
| 'edit';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
focus:outline-none focus:border-blue-400
|
||||
dark:placeholder-gray-600 dark:text-gray-500
|
||||
"
|
||||
:disabled="disabled"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
@ -36,6 +37,7 @@
|
|||
focus:outline-none focus:border-blue-400
|
||||
dark:placeholder-gray-600 dark:text-gray-500
|
||||
"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:rows="lines"
|
||||
/>
|
||||
|
@ -70,6 +72,10 @@ export default defineComponent({
|
|||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
emits: {
|
||||
|
|
|
@ -9,21 +9,22 @@
|
|||
</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="showAddRegistry"
|
||||
v-if="selectedRegistry"
|
||||
class="ml-auto"
|
||||
start-icon="list"
|
||||
start-icon="back"
|
||||
text="Show registries"
|
||||
@click="showAddRegistry = false"
|
||||
@click="selectedRegistry = undefined"
|
||||
/>
|
||||
<Button v-else class="ml-auto" start-icon="plus" text="Add registry" @click="showAddRegistry = true" />
|
||||
<Button v-else class="ml-auto" start-icon="plus" text="Add registry" @click="selectedRegistry = {}" />
|
||||
</div>
|
||||
|
||||
<div v-if="!showAddRegistry" class="space-y-4 text-gray-500">
|
||||
<div v-if="!selectedRegistry" class="space-y-4 text-gray-500">
|
||||
<ListItem v-for="registry in registries" :key="registry.id" class="items-center">
|
||||
<span>{{ registry.address }}</span>
|
||||
<IconButton icon="edit" class="ml-auto w-8 h-8" @click="selectedRegistry = registry" />
|
||||
<IconButton
|
||||
icon="trash"
|
||||
class="ml-auto w-8 h-8 hover:text-red-400"
|
||||
class="w-8 h-8 hover:text-red-400"
|
||||
:is-loading="isDeleting"
|
||||
@click="deleteRegistry(registry)"
|
||||
/>
|
||||
|
@ -36,7 +37,12 @@
|
|||
<form @submit.prevent="createRegistry">
|
||||
<InputField label="Address">
|
||||
<!-- TODO: check input field Address is a valid address -->
|
||||
<TextField v-model="selectedRegistry.address" placeholder="Registry Address (e.g. docker.io)" required />
|
||||
<TextField
|
||||
v-model="selectedRegistry.address"
|
||||
placeholder="Registry Address (e.g. docker.io)"
|
||||
required
|
||||
:disabled="isEditingRegistry"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField label="Username">
|
||||
|
@ -47,14 +53,14 @@
|
|||
<TextField v-model="selectedRegistry.password" placeholder="Password" required />
|
||||
</InputField>
|
||||
|
||||
<Button type="submit" :is-loading="isSaving" text="Add registry" />
|
||||
<Button type="submit" :is-loading="isSaving" :text="isEditingRegistry ? 'Save registy' : 'Add registry'" />
|
||||
</form>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, onMounted, Ref, ref } from 'vue';
|
||||
import { computed, defineComponent, inject, onMounted, Ref, ref } from 'vue';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import DocsLink from '~/components/atomic/DocsLink.vue';
|
||||
|
@ -88,8 +94,8 @@ export default defineComponent({
|
|||
|
||||
const repo = inject<Ref<Repo>>('repo');
|
||||
const registries = ref<Registry[]>();
|
||||
const showAddRegistry = ref(false);
|
||||
const selectedRegistry = ref<Partial<Registry>>({});
|
||||
const selectedRegistry = ref<Partial<Registry>>();
|
||||
const isEditingRegistry = computed(() => !!selectedRegistry.value?.id);
|
||||
|
||||
async function loadRegistries() {
|
||||
if (!repo?.value) {
|
||||
|
@ -104,10 +110,17 @@ export default defineComponent({
|
|||
throw new Error("Unexpected: Can't load repo");
|
||||
}
|
||||
|
||||
await apiClient.createRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
|
||||
if (!selectedRegistry.value) {
|
||||
throw new Error("Unexpected: Can't get registry");
|
||||
}
|
||||
|
||||
if (isEditingRegistry.value) {
|
||||
await apiClient.updateRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
|
||||
} else {
|
||||
await apiClient.createRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
|
||||
}
|
||||
notifications.notify({ title: 'Registry credentials created', type: 'success' });
|
||||
showAddRegistry.value = false;
|
||||
selectedRegistry.value = {};
|
||||
selectedRegistry.value = undefined;
|
||||
await loadRegistries();
|
||||
});
|
||||
|
||||
|
@ -126,7 +139,7 @@ export default defineComponent({
|
|||
await loadRegistries();
|
||||
});
|
||||
|
||||
return { selectedRegistry, registries, showAddRegistry, isSaving, isDeleting, createRegistry, deleteRegistry };
|
||||
return { selectedRegistry, registries, isEditingRegistry, isSaving, isDeleting, createRegistry, deleteRegistry };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,16 +9,16 @@
|
|||
</p>
|
||||
</div>
|
||||
<Button
|
||||
v-if="showAddSecret"
|
||||
v-if="selectedSecret"
|
||||
class="ml-auto"
|
||||
text="Show secrets"
|
||||
start-icon="list"
|
||||
@click="showAddSecret = false"
|
||||
start-icon="back"
|
||||
@click="selectedSecret = undefined"
|
||||
/>
|
||||
<Button v-else class="ml-auto" text="Add secret" start-icon="plus" @click="showAddSecret = true" />
|
||||
<Button v-else class="ml-auto" text="Add secret" start-icon="plus" @click="showAddSecret" />
|
||||
</div>
|
||||
|
||||
<div v-if="!showAddSecret" class="space-y-4 text-gray-500">
|
||||
<div v-if="!selectedSecret" class="space-y-4 text-gray-500">
|
||||
<ListItem v-for="secret in secrets" :key="secret.id" class="items-center">
|
||||
<span>{{ secret.name }}</span>
|
||||
<div class="ml-auto">
|
||||
|
@ -29,6 +29,7 @@
|
|||
>{{ event }}</span
|
||||
>
|
||||
</div>
|
||||
<IconButton icon="edit" class="ml-2 w-8 h-8" @click="selectedSecret = secret" />
|
||||
<IconButton
|
||||
icon="trash"
|
||||
class="ml-2 w-8 h-8 hover:text-red-400"
|
||||
|
@ -43,7 +44,7 @@
|
|||
<div v-else class="space-y-4">
|
||||
<form @submit.prevent="createSecret">
|
||||
<InputField label="Name">
|
||||
<TextField v-model="selectedSecret.name" placeholder="Name" required />
|
||||
<TextField v-model="selectedSecret.name" placeholder="Name" required :disabled="isEditingSecret" />
|
||||
</InputField>
|
||||
|
||||
<InputField label="Value">
|
||||
|
@ -61,14 +62,15 @@
|
|||
<CheckboxesField v-model="selectedSecret.event" :options="secretEventsOptions" />
|
||||
</InputField>
|
||||
|
||||
<Button :is-loading="isSaving" type="submit" text="Add secret" />
|
||||
<Button :is-loading="isSaving" type="submit" :text="isEditingSecret ? 'Save secret' : 'Add secret'" />
|
||||
</form>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, onMounted, Ref, ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { computed, defineComponent, inject, onMounted, Ref, ref } from 'vue';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import DocsLink from '~/components/atomic/DocsLink.vue';
|
||||
|
@ -123,9 +125,18 @@ export default defineComponent({
|
|||
|
||||
const repo = inject<Ref<Repo>>('repo');
|
||||
const secrets = ref<Secret[]>();
|
||||
const showAddSecret = ref(false);
|
||||
const selectedSecret = ref<Partial<Secret>>({ ...emptySecret });
|
||||
const images = ref('');
|
||||
const selectedSecret = ref<Partial<Secret>>();
|
||||
const isEditingSecret = computed(() => !!selectedSecret.value?.id);
|
||||
const images = computed<string>({
|
||||
get() {
|
||||
return selectedSecret.value?.image?.join(',') || '';
|
||||
},
|
||||
set(value) {
|
||||
if (selectedSecret.value) {
|
||||
selectedSecret.value.image = value.split(',').map((s) => s.trim());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function loadSecrets() {
|
||||
if (!repo?.value) {
|
||||
|
@ -140,12 +151,17 @@ export default defineComponent({
|
|||
throw new Error("Unexpected: Can't load repo");
|
||||
}
|
||||
|
||||
const imageList = images.value.split(',').map((s) => s.trim());
|
||||
selectedSecret.value.image = imageList.filter((s) => s !== '');
|
||||
await apiClient.createSecret(repo.value.owner, repo.value.name, selectedSecret.value);
|
||||
if (!selectedSecret.value) {
|
||||
throw new Error("Unexpected: Can't get secret");
|
||||
}
|
||||
|
||||
if (isEditingSecret.value) {
|
||||
await apiClient.updateSecret(repo.value.owner, repo.value.name, selectedSecret.value);
|
||||
} else {
|
||||
await apiClient.createSecret(repo.value.owner, repo.value.name, selectedSecret.value);
|
||||
}
|
||||
notifications.notify({ title: 'Secret created', type: 'success' });
|
||||
showAddSecret.value = false;
|
||||
selectedSecret.value = { ...emptySecret };
|
||||
selectedSecret.value = undefined;
|
||||
await loadSecrets();
|
||||
});
|
||||
|
||||
|
@ -159,6 +175,10 @@ export default defineComponent({
|
|||
await loadSecrets();
|
||||
});
|
||||
|
||||
function showAddSecret() {
|
||||
selectedSecret.value = cloneDeep(emptySecret);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSecrets();
|
||||
});
|
||||
|
@ -168,9 +188,10 @@ export default defineComponent({
|
|||
selectedSecret,
|
||||
secrets,
|
||||
images,
|
||||
showAddSecret,
|
||||
isEditingSecret,
|
||||
isSaving,
|
||||
isDeleting,
|
||||
showAddSecret,
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
};
|
||||
|
|
|
@ -111,6 +111,10 @@ export default class WoodpeckerClient extends ApiClient {
|
|||
return this._post(`/api/repos/${owner}/${repo}/secrets`, secret);
|
||||
}
|
||||
|
||||
updateSecret(owner: string, repo: string, secret: Partial<Secret>): Promise<unknown> {
|
||||
return this._patch(`/api/repos/${owner}/${repo}/secrets/${secret.name}`, secret);
|
||||
}
|
||||
|
||||
deleteSecret(owner: string, repo: string, secretName: string): Promise<unknown> {
|
||||
return this._delete(`/api/repos/${owner}/${repo}/secrets/${secretName}`);
|
||||
}
|
||||
|
@ -123,6 +127,10 @@ export default class WoodpeckerClient extends ApiClient {
|
|||
return this._post(`/api/repos/${owner}/${repo}/registry`, registry);
|
||||
}
|
||||
|
||||
updateRegistry(owner: string, repo: string, registry: Partial<Registry>): Promise<unknown> {
|
||||
return this._patch(`/api/repos/${owner}/${repo}/registry/${registry.address}`, registry);
|
||||
}
|
||||
|
||||
deleteRegistry(owner: string, repo: string, registryAddress: string): Promise<unknown> {
|
||||
return this._delete(`/api/repos/${owner}/${repo}/registry/${registryAddress}`);
|
||||
}
|
||||
|
|
|
@ -200,6 +200,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/lodash@4.14.179":
|
||||
version "4.14.179"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
|
||||
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
|
||||
|
||||
"@types/node-emoji@1.8.1":
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-emoji/-/node-emoji-1.8.1.tgz#689cb74fdf6e84309bcafce93a135dfecd01de3f"
|
||||
|
@ -1958,7 +1963,7 @@ lodash.truncate@^4.4.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash@^4.17.19, lodash@^4.17.21:
|
||||
lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
|
Loading…
Reference in a new issue