Add editing of secrets and registries (#823)

This commit is contained in:
Anbraten
2022-03-02 00:19:33 +01:00
committed by GitHub
parent da99f47553
commit 2f6f44417d
8 changed files with 93 additions and 36 deletions

View File

@@ -25,6 +25,7 @@
"fuse.js": "6.4.6", "fuse.js": "6.4.6",
"humanize-duration": "3.27.0", "humanize-duration": "3.27.0",
"javascript-time-ago": "2.3.10", "javascript-time-ago": "2.3.10",
"lodash": "4.17.21",
"node-emoji": "1.11.0", "node-emoji": "1.11.0",
"pinia": "2.0.0", "pinia": "2.0.0",
"vue": "v3.2.20", "vue": "v3.2.20",
@@ -34,6 +35,7 @@
"@iconify/json": "1.1.421", "@iconify/json": "1.1.421",
"@types/humanize-duration": "3.27.0", "@types/humanize-duration": "3.27.0",
"@types/javascript-time-ago": "2.0.3", "@types/javascript-time-ago": "2.0.3",
"@types/lodash": "4.14.179",
"@types/node": "16.11.6", "@types/node": "16.11.6",
"@types/node-emoji": "1.8.1", "@types/node-emoji": "1.8.1",
"@typescript-eslint/eslint-plugin": "5.6.0", "@typescript-eslint/eslint-plugin": "5.6.0",

View File

@@ -32,9 +32,9 @@
@click="doClick" @click="doClick"
> >
<slot> <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> <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 <div
class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center" class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center"
:class="{ :class="{

View File

@@ -33,6 +33,7 @@
<i-bx-bx-power-off v-else-if="name === 'turn-off'" class="h-6 w-6" /> <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-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-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" /> <div v-else-if="name === 'blank'" class="h-6 w-6" />
</template> </template>
@@ -74,7 +75,8 @@ export type IconNames =
| 'heal' | 'heal'
| 'chevron-right' | 'chevron-right'
| 'turn-off' | 'turn-off'
| 'close'; | 'close'
| 'edit';
export default defineComponent({ export default defineComponent({
name: 'Icon', name: 'Icon',

View File

@@ -22,6 +22,7 @@
focus:outline-none focus:border-blue-400 focus:outline-none focus:border-blue-400
dark:placeholder-gray-600 dark:text-gray-500 dark:placeholder-gray-600 dark:text-gray-500
" "
:disabled="disabled"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
/> />
@@ -36,6 +37,7 @@
focus:outline-none focus:border-blue-400 focus:outline-none focus:border-blue-400
dark:placeholder-gray-600 dark:text-gray-500 dark:placeholder-gray-600 dark:text-gray-500
" "
:disabled="disabled"
:placeholder="placeholder" :placeholder="placeholder"
:rows="lines" :rows="lines"
/> />
@@ -70,6 +72,10 @@ export default defineComponent({
type: Number, type: Number,
default: 1, default: 1,
}, },
disabled: {
type: Boolean,
},
}, },
emits: { emits: {

View File

@@ -9,21 +9,22 @@
</p> </p>
</div> </div>
<Button <Button
v-if="showAddRegistry" v-if="selectedRegistry"
class="ml-auto" class="ml-auto"
start-icon="list" start-icon="back"
text="Show registries" 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>
<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"> <ListItem v-for="registry in registries" :key="registry.id" class="items-center">
<span>{{ registry.address }}</span> <span>{{ registry.address }}</span>
<IconButton icon="edit" class="ml-auto w-8 h-8" @click="selectedRegistry = registry" />
<IconButton <IconButton
icon="trash" 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" :is-loading="isDeleting"
@click="deleteRegistry(registry)" @click="deleteRegistry(registry)"
/> />
@@ -36,7 +37,12 @@
<form @submit.prevent="createRegistry"> <form @submit.prevent="createRegistry">
<InputField label="Address"> <InputField label="Address">
<!-- TODO: check input field Address is a valid 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>
<InputField label="Username"> <InputField label="Username">
@@ -47,14 +53,14 @@
<TextField v-model="selectedRegistry.password" placeholder="Password" required /> <TextField v-model="selectedRegistry.password" placeholder="Password" required />
</InputField> </InputField>
<Button type="submit" :is-loading="isSaving" text="Add registry" /> <Button type="submit" :is-loading="isSaving" :text="isEditingRegistry ? 'Save registy' : 'Add registry'" />
</form> </form>
</div> </div>
</Panel> </Panel>
</template> </template>
<script lang="ts"> <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 Button from '~/components/atomic/Button.vue';
import DocsLink from '~/components/atomic/DocsLink.vue'; import DocsLink from '~/components/atomic/DocsLink.vue';
@@ -88,8 +94,8 @@ export default defineComponent({
const repo = inject<Ref<Repo>>('repo'); const repo = inject<Ref<Repo>>('repo');
const registries = ref<Registry[]>(); 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() { async function loadRegistries() {
if (!repo?.value) { if (!repo?.value) {
@@ -104,10 +110,17 @@ export default defineComponent({
throw new Error("Unexpected: Can't load repo"); throw new Error("Unexpected: Can't load repo");
} }
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); await apiClient.createRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
}
notifications.notify({ title: 'Registry credentials created', type: 'success' }); notifications.notify({ title: 'Registry credentials created', type: 'success' });
showAddRegistry.value = false; selectedRegistry.value = undefined;
selectedRegistry.value = {};
await loadRegistries(); await loadRegistries();
}); });
@@ -126,7 +139,7 @@ export default defineComponent({
await loadRegistries(); await loadRegistries();
}); });
return { selectedRegistry, registries, showAddRegistry, isSaving, isDeleting, createRegistry, deleteRegistry }; return { selectedRegistry, registries, isEditingRegistry, isSaving, isDeleting, createRegistry, deleteRegistry };
}, },
}); });
</script> </script>

View File

@@ -9,16 +9,16 @@
</p> </p>
</div> </div>
<Button <Button
v-if="showAddSecret" v-if="selectedSecret"
class="ml-auto" class="ml-auto"
text="Show secrets" text="Show secrets"
start-icon="list" start-icon="back"
@click="showAddSecret = false" @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>
<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"> <ListItem v-for="secret in secrets" :key="secret.id" class="items-center">
<span>{{ secret.name }}</span> <span>{{ secret.name }}</span>
<div class="ml-auto"> <div class="ml-auto">
@@ -29,6 +29,7 @@
>{{ event }}</span >{{ event }}</span
> >
</div> </div>
<IconButton icon="edit" class="ml-2 w-8 h-8" @click="selectedSecret = secret" />
<IconButton <IconButton
icon="trash" icon="trash"
class="ml-2 w-8 h-8 hover:text-red-400" class="ml-2 w-8 h-8 hover:text-red-400"
@@ -43,7 +44,7 @@
<div v-else class="space-y-4"> <div v-else class="space-y-4">
<form @submit.prevent="createSecret"> <form @submit.prevent="createSecret">
<InputField label="Name"> <InputField label="Name">
<TextField v-model="selectedSecret.name" placeholder="Name" required /> <TextField v-model="selectedSecret.name" placeholder="Name" required :disabled="isEditingSecret" />
</InputField> </InputField>
<InputField label="Value"> <InputField label="Value">
@@ -61,14 +62,15 @@
<CheckboxesField v-model="selectedSecret.event" :options="secretEventsOptions" /> <CheckboxesField v-model="selectedSecret.event" :options="secretEventsOptions" />
</InputField> </InputField>
<Button :is-loading="isSaving" type="submit" text="Add secret" /> <Button :is-loading="isSaving" type="submit" :text="isEditingSecret ? 'Save secret' : 'Add secret'" />
</form> </form>
</div> </div>
</Panel> </Panel>
</template> </template>
<script lang="ts"> <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 Button from '~/components/atomic/Button.vue';
import DocsLink from '~/components/atomic/DocsLink.vue'; import DocsLink from '~/components/atomic/DocsLink.vue';
@@ -123,9 +125,18 @@ export default defineComponent({
const repo = inject<Ref<Repo>>('repo'); const repo = inject<Ref<Repo>>('repo');
const secrets = ref<Secret[]>(); const secrets = ref<Secret[]>();
const showAddSecret = ref(false); const selectedSecret = ref<Partial<Secret>>();
const selectedSecret = ref<Partial<Secret>>({ ...emptySecret }); const isEditingSecret = computed(() => !!selectedSecret.value?.id);
const images = ref(''); 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() { async function loadSecrets() {
if (!repo?.value) { if (!repo?.value) {
@@ -140,12 +151,17 @@ export default defineComponent({
throw new Error("Unexpected: Can't load repo"); throw new Error("Unexpected: Can't load repo");
} }
const imageList = images.value.split(',').map((s) => s.trim()); if (!selectedSecret.value) {
selectedSecret.value.image = imageList.filter((s) => s !== ''); 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); await apiClient.createSecret(repo.value.owner, repo.value.name, selectedSecret.value);
}
notifications.notify({ title: 'Secret created', type: 'success' }); notifications.notify({ title: 'Secret created', type: 'success' });
showAddSecret.value = false; selectedSecret.value = undefined;
selectedSecret.value = { ...emptySecret };
await loadSecrets(); await loadSecrets();
}); });
@@ -159,6 +175,10 @@ export default defineComponent({
await loadSecrets(); await loadSecrets();
}); });
function showAddSecret() {
selectedSecret.value = cloneDeep(emptySecret);
}
onMounted(async () => { onMounted(async () => {
await loadSecrets(); await loadSecrets();
}); });
@@ -168,9 +188,10 @@ export default defineComponent({
selectedSecret, selectedSecret,
secrets, secrets,
images, images,
showAddSecret, isEditingSecret,
isSaving, isSaving,
isDeleting, isDeleting,
showAddSecret,
createSecret, createSecret,
deleteSecret, deleteSecret,
}; };

View File

@@ -111,6 +111,10 @@ export default class WoodpeckerClient extends ApiClient {
return this._post(`/api/repos/${owner}/${repo}/secrets`, secret); 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> { deleteSecret(owner: string, repo: string, secretName: string): Promise<unknown> {
return this._delete(`/api/repos/${owner}/${repo}/secrets/${secretName}`); 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); 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> { deleteRegistry(owner: string, repo: string, registryAddress: string): Promise<unknown> {
return this._delete(`/api/repos/${owner}/${repo}/registry/${registryAddress}`); return this._delete(`/api/repos/${owner}/${repo}/registry/${registryAddress}`);
} }

View File

@@ -200,6 +200,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= 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": "@types/node-emoji@1.8.1":
version "1.8.1" version "1.8.1"
resolved "https://registry.yarnpkg.com/@types/node-emoji/-/node-emoji-1.8.1.tgz#689cb74fdf6e84309bcafce93a135dfecd01de3f" 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" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= 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" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==