Access repos by their ids (#1691)

closes #1295 
closes #648

# TODO
- [x] add new routes with `:repoID`
- [x] load repo in middleware using `:repoID` if present
- [x] update UI routes `:owner/:name` to `:repoID`
- [x] load repos using id in UI
- [x] add lookup endpoint `:owner/:name` to `:repoID`
- [x] redirect `:owner/:name` to `:repoID` in UI
- [x] use badge with `:repoID` route in UI
- [x] update `woodpecker-go`
- [x] check cli
- [x] add migrations / deprecation notes
- [x] check if #648 got solved directly
- [x] Test
  - [x] create repo
  - [x] repo pages
  - [x] ui redirects
  - [x] forge status links
This commit is contained in:
Anbraten
2023-06-12 16:07:52 -07:00
committed by GitHub
parent e3593cd9a4
commit ff01a9ff1d
98 changed files with 1402 additions and 1676 deletions

View File

@@ -99,12 +99,7 @@ function deleteVar(key: string) {
const pipelineNumber = toRef(props, 'pipelineNumber');
async function triggerDeployPipeline() {
loading.value = true;
const newPipeline = await apiClient.deployPipeline(
repo.value.owner,
repo.value.name,
pipelineNumber.value,
payload.value,
);
const newPipeline = await apiClient.deployPipeline(repo.value.id, pipelineNumber.value, payload.value);
emit('close');

View File

@@ -80,7 +80,7 @@ const newPipelineVariable = ref<{ name: string; value: string }>({ name: '', val
const loading = ref(true);
onMounted(async () => {
const data = await usePaginate((page) => apiClient.getRepoBranches(repo.value.owner, repo.value.name, page));
const data = await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, page));
branches.value = data.map((e) => ({
text: e,
value: e,
@@ -103,7 +103,7 @@ function deleteVar(key: string) {
async function triggerManualPipeline() {
loading.value = true;
const pipeline = await apiClient.createPipeline(repo.value.owner, repo.value.name, payload.value);
const pipeline = await apiClient.createPipeline(repo.value.id, payload.value);
emit('close');

View File

@@ -2,7 +2,7 @@
<div v-if="pipeline" class="flex text-color w-full">
<PipelineStatusIcon :status="pipeline.status" class="flex items-center" />
<div class="flex flex-col ml-4 min-w-0">
<span class="underline">{{ pipeline.owner }} / {{ pipeline.name }}</span>
<span class="underline">{{ repo?.owner }} / {{ repo?.name }}</span>
<span class="whitespace-nowrap overflow-hidden overflow-ellipsis">{{ message }}</span>
<div class="flex flex-col mt-2">
<div class="flex space-x-2 items-center">
@@ -23,32 +23,24 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { Tooltip } from 'floating-vue';
import { defineComponent, PropType, toRef } from 'vue';
import { computed, toRef } from 'vue';
import Icon from '~/components/atomic/Icon.vue';
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
import usePipeline from '~/compositions/usePipeline';
import { PipelineFeed } from '~/lib/api/types';
import { useRepoStore } from '~/store/repos';
export default defineComponent({
name: 'PipelineFeedItem',
const props = defineProps<{
pipeline: PipelineFeed;
}>();
components: { PipelineStatusIcon, Icon, Tooltip },
const repoStore = useRepoStore();
props: {
pipeline: {
type: Object as PropType<PipelineFeed>,
required: true,
},
},
const pipeline = toRef(props, 'pipeline');
const repo = repoStore.getRepo(computed(() => pipeline.value.repo_id));
setup(props) {
const pipeline = toRef(props, 'pipeline');
const { since, duration, message, created } = usePipeline(pipeline);
return { since, duration, message, created };
},
});
const { since, duration, message, created } = usePipeline(pipeline);
</script>

View File

@@ -1,43 +1,29 @@
<template>
<aside
v-if="isPipelineFeedOpen"
v-if="isOpen"
class="flex flex-col z-50 overflow-y-auto items-center bg-white dark:bg-dark-gray-800 dark:border-dark-500"
:aria-label="$t('pipeline_feed')"
>
<router-link
v-for="pipeline in sortedPipelineFeed"
v-for="pipeline in sortedPipelines"
:key="pipeline.id"
:to="{
name: 'repo-pipeline',
params: { repoOwner: pipeline.owner, repoName: pipeline.name, pipelineId: pipeline.number },
params: { repoId: pipeline.repo_id, pipelineId: pipeline.number },
}"
class="flex border-b py-4 px-2 w-full hover:bg-light-300 dark:hover:bg-dark-gray-900 dark:border-dark-gray-600 hover:shadow-sm"
>
<PipelineFeedItem :pipeline="pipeline" />
</router-link>
<span v-if="sortedPipelineFeed.length === 0" class="text-color m-4">{{ $t('repo.pipeline.no_pipelines') }}</span>
<span v-if="sortedPipelines.length === 0" class="text-color m-4">{{ $t('repo.pipeline.no_pipelines') }}</span>
</aside>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import PipelineFeedItem from '~/components/pipeline-feed/PipelineFeedItem.vue';
import usePipelineFeed from '~/compositions/usePipelineFeed';
export default defineComponent({
name: 'PipelineFeedSidebar',
components: { PipelineFeedItem },
setup() {
const pipelineFeed = usePipelineFeed();
return {
isPipelineFeedOpen: pipelineFeed.isOpen,
sortedPipelineFeed: pipelineFeed.sortedPipelines,
};
},
});
const pipelineFeed = usePipelineFeed();
const { isOpen, sortedPipelines } = pipelineFeed;
</script>

View File

@@ -5,7 +5,7 @@
:key="pipeline.id"
:to="{
name: 'repo-pipeline',
params: { repoOwner: repo.owner, repoName: repo.name, pipelineId: pipeline.number },
params: { pipelineId: pipeline.number },
}"
:pipeline="pipeline"
/>
@@ -15,28 +15,12 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script lang="ts" setup>
import Panel from '~/components/layout/Panel.vue';
import PipelineItem from '~/components/repo/pipeline/PipelineItem.vue';
import { Pipeline, Repo } from '~/lib/api/types';
import { Pipeline } from '~/lib/api/types';
export default defineComponent({
name: 'PipelineList',
components: { Panel, PipelineItem },
props: {
repo: {
type: Object as PropType<Repo>,
required: true,
},
pipelines: {
type: Object as PropType<Pipeline[] | undefined>,
required: true,
},
},
});
defineProps<{
pipelines: Pipeline[] | undefined;
}>();
</script>

View File

@@ -190,7 +190,7 @@ async function download() {
let logs;
try {
downloadInProgress.value = true;
logs = await apiClient.getLogs(repo.value.owner, repo.value.name, pipeline.value.number, step.value.id);
logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id);
} catch (e) {
notifications.notifyError(e, i18n.t('repo.pipeline.log_download_error'));
return;
@@ -239,22 +239,16 @@ async function loadLogs() {
}
if (isStepFinished(step.value)) {
const logs = await apiClient.getLogs(repo.value.owner, repo.value.name, pipeline.value.number, step.value.id);
const logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id);
logs?.forEach((line) => writeLog({ index: line.line, text: atob(line.data), time: line.time }));
flushLogs(false);
}
if (isStepRunning(step.value)) {
stream.value = apiClient.streamLogs(
repo.value.owner,
repo.value.name,
pipeline.value.number,
step.value.id,
(line) => {
writeLog({ index: line.line, text: atob(line.data), time: line.time });
flushLogs(true);
},
);
stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => {
writeLog({ index: line.line, text: atob(line.data), time: line.time });
flushLogs(true);
});
}
}

View File

@@ -45,8 +45,8 @@
</Panel>
</template>
<script lang="ts">
import { defineComponent, inject, Ref } from 'vue';
<script lang="ts" setup>
import { computed, inject, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
@@ -57,74 +57,56 @@ import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { Repo } from '~/lib/api/types';
export default defineComponent({
name: 'ActionsTab',
const apiClient = useApiClient();
const router = useRouter();
const notifications = useNotifications();
const i18n = useI18n();
components: { Button, Panel },
const repo = inject<Ref<Repo>>('repo');
setup() {
const apiClient = useApiClient();
const router = useRouter();
const notifications = useNotifications();
const i18n = useI18n();
const { doSubmit: repairRepo, isLoading: isRepairingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
const repo = inject<Ref<Repo>>('repo');
const { doSubmit: repairRepo, isLoading: isRepairingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.repairRepo(repo.value.owner, repo.value.name);
notifications.notify({ title: i18n.t('repo.settings.actions.repair.success'), type: 'success' });
});
const { doSubmit: deleteRepo, isLoading: isDeletingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
// TODO use proper dialog
// eslint-disable-next-line no-alert, no-restricted-globals
if (!confirm(i18n.t('repo.settings.actions.delete.confirm'))) {
return;
}
await apiClient.deleteRepo(repo.value.owner, repo.value.name);
notifications.notify({ title: i18n.t('repo.settings.actions.delete.success'), type: 'success' });
await router.replace({ name: 'repos' });
});
const { doSubmit: activateRepo, isLoading: isActivatingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.activateRepo(repo.value.owner, repo.value.name);
notifications.notify({ title: i18n.t('repo.settings.actions.enable.success'), type: 'success' });
});
const { doSubmit: deactivateRepo, isLoading: isDeactivatingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.deleteRepo(repo.value.owner, repo.value.name, false);
notifications.notify({ title: i18n.t('repo.settings.actions.disable.success'), type: 'success' });
await router.replace({ name: 'repos' });
});
return {
isActive: repo?.value.active,
isRepairingRepo,
isDeletingRepo,
isDeactivatingRepo,
isActivatingRepo,
deleteRepo,
repairRepo,
deactivateRepo,
activateRepo,
};
},
await apiClient.repairRepo(repo.value.id);
notifications.notify({ title: i18n.t('repo.settings.actions.repair.success'), type: 'success' });
});
const { doSubmit: deleteRepo, isLoading: isDeletingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
// TODO use proper dialog
// eslint-disable-next-line no-alert, no-restricted-globals
if (!confirm(i18n.t('repo.settings.actions.delete.confirm'))) {
return;
}
await apiClient.deleteRepo(repo.value.id);
notifications.notify({ title: i18n.t('repo.settings.actions.delete.success'), type: 'success' });
await router.replace({ name: 'repos' });
});
const { doSubmit: activateRepo, isLoading: isActivatingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.activateRepo(repo.value.forge_remote_id);
notifications.notify({ title: i18n.t('repo.settings.actions.enable.success'), type: 'success' });
});
const { doSubmit: deactivateRepo, isLoading: isDeactivatingRepo } = useAsyncAction(async () => {
if (!repo) {
throw new Error('Unexpected: Repo should be set');
}
await apiClient.deleteRepo(repo.value.id, false);
notifications.notify({ title: i18n.t('repo.settings.actions.disable.success'), type: 'success' });
await router.replace({ name: 'repos' });
});
const isActive = computed(() => repo?.value.active);
</script>

View File

@@ -75,7 +75,7 @@ export default defineComponent({
throw new Error('Unexpected: "repo" should be provided at this place');
}
branches.value = (await usePaginate((page) => apiClient.getRepoBranches(repo.value.owner, repo.value.name, page)))
branches.value = (await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, page)))
.map((b) => ({
value: b,
text: b,
@@ -91,14 +91,9 @@ export default defineComponent({
window.location.port ? `:${window.location.port}` : ''
}`;
const badgeUrl = computed(
() =>
`/api/badges/${repo.value.owner}/${repo.value.name}/status.svg${
branch.value !== '' ? `?branch=${branch.value}` : ''
}`,
);
const repoUrl = computed(
() => `/${repo.value.owner}/${repo.value.name}${branch.value !== '' ? `/branches/${branch.value}` : ''}`,
() => `/api/badges/${repo.value.id}/status.svg${branch.value !== '' ? `?branch=${branch.value}` : ''}`,
);
const repoUrl = computed(() => `/${repo.value.id}${branch.value !== '' ? `/branches/${branch.value}` : ''}`);
const badgeContent = computed(() => {
if (!repo) {

View File

@@ -121,7 +121,7 @@ async function loadCrons(page: number): Promise<Cron[] | null> {
throw new Error("Unexpected: Can't load repo");
}
return apiClient.getCronList(repo.value.owner, repo.value.name, page);
return apiClient.getCronList(repo.value.id, page);
}
const { resetPage, data: crons } = usePagination(loadCrons, () => !selectedCron.value);
@@ -136,9 +136,9 @@ const { doSubmit: createCron, isLoading: isSaving } = useAsyncAction(async () =>
}
if (isEditingCron.value) {
await apiClient.updateCron(repo.value.owner, repo.value.name, selectedCron.value);
await apiClient.updateCron(repo.value.id, selectedCron.value);
} else {
await apiClient.createCron(repo.value.owner, repo.value.name, selectedCron.value);
await apiClient.createCron(repo.value.id, selectedCron.value);
}
notifications.notify({
title: i18n.t(isEditingCron.value ? 'repo.settings.crons.saved' : i18n.t('repo.settings.crons.created')),
@@ -153,7 +153,7 @@ const { doSubmit: deleteCron, isLoading: isDeleting } = useAsyncAction(async (_c
throw new Error("Unexpected: Can't load repo");
}
await apiClient.deleteCron(repo.value.owner, repo.value.name, _cron.id);
await apiClient.deleteCron(repo.value.id, _cron.id);
notifications.notify({ title: i18n.t('repo.settings.crons.deleted'), type: 'success' });
resetPage();
});
@@ -163,12 +163,10 @@ const { doSubmit: runCron } = useAsyncAction(async (_cron: Cron) => {
throw new Error("Unexpected: Can't load repo");
}
const pipeline = await apiClient.runCron(repo.value.owner, repo.value.name, _cron.id);
const pipeline = await apiClient.runCron(repo.value.id, _cron.id);
await router.push({
name: 'repo-pipeline',
params: {
repoOwner: repo.value.owner,
repoName: repo.value.name,
pipelineId: pipeline.number,
},
});

View File

@@ -148,7 +148,7 @@ export default defineComponent({
throw new Error('Unexpected: Repo should be set');
}
await repoStore.loadRepo(repo.value.owner, repo.value.name);
await repoStore.loadRepo(repo.value.id);
loadRepoSettings();
}
@@ -161,7 +161,7 @@ export default defineComponent({
throw new Error('Unexpected: Repo-Settings should be set');
}
await apiClient.updateRepo(repo.value.owner, repo.value.name, repoSettings.value);
await apiClient.updateRepo(repo.value.id, repoSettings.value);
await loadRepo();
notifications.notify({ title: i18n.t('repo.settings.general.success'), type: 'success' });
});

View File

@@ -124,7 +124,7 @@ export default defineComponent({
throw new Error("Unexpected: Can't load repo");
}
return apiClient.getRegistryList(repo.value.owner, repo.value.name, page);
return apiClient.getRegistryList(repo.value.id, page);
}
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);
@@ -139,9 +139,9 @@ export default defineComponent({
}
if (isEditingRegistry.value) {
await apiClient.updateRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
await apiClient.updateRegistry(repo.value.id, selectedRegistry.value);
} else {
await apiClient.createRegistry(repo.value.owner, repo.value.name, selectedRegistry.value);
await apiClient.createRegistry(repo.value.id, selectedRegistry.value);
}
notifications.notify({
title: i18n.t(
@@ -159,7 +159,7 @@ export default defineComponent({
}
const registryAddress = encodeURIComponent(_registry.address);
await apiClient.deleteRegistry(repo.value.owner, repo.value.name, registryAddress);
await apiClient.deleteRegistry(repo.value.id, registryAddress);
notifications.notify({ title: i18n.t('repo.settings.registries.deleted'), type: 'success' });
resetPage();
});

View File

@@ -86,7 +86,7 @@ export default defineComponent({
throw new Error("Unexpected: Can't load repo");
}
return apiClient.getSecretList(repo.value.owner, repo.value.name, page);
return apiClient.getSecretList(repo.value.id, page);
}
const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);
@@ -101,9 +101,9 @@ export default defineComponent({
}
if (isEditingSecret.value) {
await apiClient.updateSecret(repo.value.owner, repo.value.name, selectedSecret.value);
await apiClient.updateSecret(repo.value.id, selectedSecret.value);
} else {
await apiClient.createSecret(repo.value.owner, repo.value.name, selectedSecret.value);
await apiClient.createSecret(repo.value.id, selectedSecret.value);
}
notifications.notify({
title: i18n.t(isEditingSecret.value ? 'repo.settings.secrets.saved' : 'repo.settings.secrets.created'),
@@ -118,7 +118,7 @@ export default defineComponent({
throw new Error("Unexpected: Can't load repo");
}
await apiClient.deleteSecret(repo.value.owner, repo.value.name, _secret.name);
await apiClient.deleteSecret(repo.value.id, _secret.name);
notifications.notify({ title: i18n.t('repo.settings.secrets.deleted'), type: 'success' });
resetPage();
});