Merge branch 'origin/main' into 'next-release/main'

This commit is contained in:
oauth 2025-01-02 13:31:24 +00:00
commit e8328512ab
9 changed files with 69 additions and 117 deletions

View file

@ -48,6 +48,7 @@
"dind", "dind",
"Dockle", "Dockle",
"doublestar", "doublestar",
"emojify",
"envsubst", "envsubst",
"errgroup", "errgroup",
"estree", "estree",

View file

@ -121,8 +121,7 @@ import IconButton from '~/components/atomic/IconButton.vue';
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue'; import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import type { Pipeline, Repo, RepoPermissions } from '~/lib/api/types'; import type { Pipeline, PipelineStep, PipelineWorkflow, Repo, RepoPermissions } from '~/lib/api/types';
import { findStep, isStepFinished, isStepRunning } from '~/utils/helpers';
interface LogLine { interface LogLine {
index: number; index: number;
@ -303,12 +302,12 @@ async function loadLogs() {
return; return;
} }
if (isStepFinished(step.value)) { if (step.value.state !== 'running' && step.value.state !== 'pending') {
loadedStepSlug.value = stepSlug.value; loadedStepSlug.value = stepSlug.value;
const logs = await apiClient.getLogs(repo.value.id, 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: line.data, time: line.time })); logs?.forEach((line) => writeLog({ index: line.line, text: line.data, time: line.time }));
flushLogs(false); flushLogs(false);
} else if (step.value.state === 'pending' || isStepRunning(step.value)) { } else {
loadedStepSlug.value = stepSlug.value; loadedStepSlug.value = stepSlug.value;
stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => { stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => {
writeLog({ index: line.line, text: line.data, time: line.time }); writeLog({ index: line.line, text: line.data, time: line.time });
@ -336,6 +335,29 @@ async function deleteLogs() {
} }
} }
function findStep(workflows: PipelineWorkflow[], pid: number): PipelineStep | undefined {
return workflows.reduce(
(prev, workflow) => {
const result = workflow.children.reduce(
(prevChild, step) => {
if (step.pid === pid) {
return step;
}
return prevChild;
},
undefined as PipelineStep | undefined,
);
if (result) {
return result;
}
return prev;
},
undefined as PipelineStep | undefined,
);
}
onMounted(async () => { onMounted(async () => {
await loadLogs(); await loadLogs();
}); });

View file

@ -87,7 +87,7 @@ function durationAsNumber(durationMs: number): string {
} }
export function useDate() { export function useDate() {
async function setDayjsLocale(locale: string) { async function setDateLocale(locale: string) {
currentLocale = locale; currentLocale = locale;
} }
@ -95,7 +95,7 @@ export function useDate() {
toLocaleString, toLocaleString,
timeAgo, timeAgo,
prettyDuration, prettyDuration,
setDayjsLocale, setDateLocale,
durationAsNumber, durationAsNumber,
}; };
} }

View file

@ -1,11 +1,17 @@
import { useStorage } from '@vueuse/core';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { getUserLanguage } from '~/utils/locale';
import { useDate } from './useDate'; import { useDate } from './useDate';
const { setDayjsLocale } = useDate(); export function getUserLanguage(): string {
const browserLocale = navigator.language.split('-')[0];
const selectedLocale = useStorage('woodpecker:locale', browserLocale).value;
return selectedLocale;
}
const { setDateLocale } = useDate();
const userLanguage = getUserLanguage(); const userLanguage = getUserLanguage();
const fallbackLocale = 'en'; const fallbackLocale = 'en';
export const i18n = createI18n({ export const i18n = createI18n({
@ -28,9 +34,9 @@ export const setI18nLanguage = async (lang: string): Promise<void> => {
await loadLocaleMessages(lang); await loadLocaleMessages(lang);
} }
i18n.global.locale.value = lang; i18n.global.locale.value = lang;
await setDayjsLocale(lang); await setDateLocale(lang);
}; };
loadLocaleMessages(fallbackLocale).catch(console.error); loadLocaleMessages(fallbackLocale).catch(console.error);
loadLocaleMessages(userLanguage).catch(console.error); loadLocaleMessages(userLanguage).catch(console.error);
setDayjsLocale(userLanguage).catch(console.error); setDateLocale(userLanguage).catch(console.error);

View file

@ -1,10 +1,10 @@
import { emojify } from 'node-emoji';
import { computed, type Ref } from 'vue'; import { computed, type Ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useDate } from '~/compositions/useDate'; import { useDate } from '~/compositions/useDate';
import { useElapsedTime } from '~/compositions/useElapsedTime'; import { useElapsedTime } from '~/compositions/useElapsedTime';
import type { Pipeline } from '~/lib/api/types'; import type { Pipeline } from '~/lib/api/types';
import { convertEmojis } from '~/utils/emoji';
const { toLocaleString, timeAgo, prettyDuration } = useDate(); const { toLocaleString, timeAgo, prettyDuration } = useDate();
@ -74,10 +74,10 @@ export default (pipeline: Ref<Pipeline | undefined>) => {
return prettyDuration(durationElapsed.value); return prettyDuration(durationElapsed.value);
}); });
const message = computed(() => convertEmojis(pipeline.value?.message ?? '')); const message = computed(() => emojify(pipeline.value?.message ?? ''));
const shortMessage = computed(() => message.value.split('\n')[0]); const shortMessage = computed(() => message.value.split('\n')[0]);
const prTitleWithDescription = computed(() => convertEmojis(pipeline.value?.title ?? '')); const prTitleWithDescription = computed(() => emojify(pipeline.value?.title ?? ''));
const prTitle = computed(() => prTitleWithDescription.value.split('\n')[0]); const prTitle = computed(() => prTitleWithDescription.value.split('\n')[0]);
const prettyRef = computed(() => { const prettyRef = computed(() => {

View file

@ -4,7 +4,29 @@ import { computed, reactive, ref, type Ref } from 'vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import type { Pipeline, PipelineFeed, PipelineWorkflow } from '~/lib/api/types'; import type { Pipeline, PipelineFeed, PipelineWorkflow } from '~/lib/api/types';
import { useRepoStore } from '~/store/repos'; import { useRepoStore } from '~/store/repos';
import { comparePipelines, comparePipelinesWithStatus, isPipelineActive } from '~/utils/helpers';
/**
* Compare two pipelines by creation timestamp.
* @param {object} a - A pipeline.
* @param {object} b - A pipeline.
* @returns {number} 0 if created at the same time, < 0 if b was create before a, > 0 otherwise
*/
function comparePipelines(a: Pipeline, b: Pipeline): number {
return (b.created || -1) - (a.created || -1);
}
/**
* Compare two pipelines by the status.
* Giving pending, running, or started higher priority than other status
* @param {object} a - A pipeline.
* @param {object} b - A pipeline.
* @returns {number} 0 if status same priority, < 0 if b has higher priority, > 0 otherwise
*/
function comparePipelinesWithStatus(a: Pipeline, b: Pipeline): number {
const bPriority = ['pending', 'running', 'started'].includes(b.status) ? 1 : 0;
const aPriority = ['pending', 'running', 'started'].includes(a.status) ? 1 : 0;
return bPriority - aPriority || comparePipelines(a, b);
}
export const usePipelineStore = defineStore('pipelines', () => { export const usePipelineStore = defineStore('pipelines', () => {
const apiClient = useApiClient(); const apiClient = useApiClient();
@ -87,7 +109,9 @@ export const usePipelineStore = defineStore('pipelines', () => {
.filter((pipeline) => repoStore.ownedRepoIds.includes(pipeline.repo_id)), .filter((pipeline) => repoStore.ownedRepoIds.includes(pipeline.repo_id)),
); );
const activePipelines = computed(() => pipelineFeed.value.filter(isPipelineActive)); const activePipelines = computed(() =>
pipelineFeed.value.filter((pipeline) => ['pending', 'running', 'started'].includes(pipeline.status)),
);
async function loadPipelineFeed() { async function loadPipelineFeed() {
await repoStore.loadRepos(); await repoStore.loadRepos();

View file

@ -1,6 +0,0 @@
// cSpell:ignore emojify
import { emojify } from 'node-emoji';
export function convertEmojis(input: string): string {
return emojify(input);
}

View file

@ -1,79 +0,0 @@
import type { Pipeline, PipelineStep, PipelineWorkflow, Repo } from '~/lib/api/types';
export function findStep(workflows: PipelineWorkflow[], pid: number): PipelineStep | undefined {
return workflows.reduce(
(prev, workflow) => {
const result = workflow.children.reduce(
(prevChild, step) => {
if (step.pid === pid) {
return step;
}
return prevChild;
},
undefined as PipelineStep | undefined,
);
if (result) {
return result;
}
return prev;
},
undefined as PipelineStep | undefined,
);
}
/**
* @param {object} step - The process object.
* @returns {boolean} true if the process is in a completed state
*/
export function isStepFinished(step: PipelineStep): boolean {
return step.state !== 'running' && step.state !== 'pending';
}
/**
* @param {object} step - The process object.
* @returns {boolean} true if the process is running
*/
export function isStepRunning(step: PipelineStep): boolean {
return step.state === 'running';
}
/**
* Compare two pipelines by creation timestamp.
* @param {object} a - A pipeline.
* @param {object} b - A pipeline.
* @returns {number} 0 if created at the same time, < 0 if b was create before a, > 0 otherwise
*/
export function comparePipelines(a: Pipeline, b: Pipeline): number {
return (b.created || -1) - (a.created || -1);
}
/**
* Compare two pipelines by the status.
* Giving pending, running, or started higher priority than other status
* @param {object} a - A pipeline.
* @param {object} b - A pipeline.
* @returns {number} 0 if status same priority, < 0 if b has higher priority, > 0 otherwise
*/
export function comparePipelinesWithStatus(a: Pipeline, b: Pipeline): number {
const bPriority = ['pending', 'running', 'started'].includes(b.status) ? 1 : 0;
const aPriority = ['pending', 'running', 'started'].includes(a.status) ? 1 : 0;
return bPriority - aPriority || comparePipelines(a, b);
}
export function isPipelineActive(pipeline: Pipeline): boolean {
return ['pending', 'running', 'started'].includes(pipeline.status);
}
export function repoSlug(ownerOrRepo: string | Repo, name?: string): string {
if (typeof ownerOrRepo === 'string') {
if (name === undefined) {
throw new Error('Please provide a name as well');
}
return `${ownerOrRepo}/${name}`;
}
return `${ownerOrRepo.owner}/${ownerOrRepo.name}`;
}

View file

@ -1,16 +0,0 @@
import { useStorage } from '@vueuse/core';
export function getUserLanguage(): string {
const browserLocale = navigator.language.split('-')[0];
const selectedLocale = useStorage('woodpecker:locale', browserLocale).value;
return selectedLocale;
}
export function truncate(text: string, length: number): string {
if (text.length <= length) {
return text;
}
return `${text.slice(0, length)}...`;
}