diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 4df57fb0b..325731e1b 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -67,6 +67,21 @@ "success": "Repository enabled" }, "open_in_forge": "Open repository in forge", + "visibility": { + "visibility": "Project visibility", + "public": { + "public": "Public", + "desc": "Every user can see your project without being logged in." + }, + "private": { + "private": "Private", + "desc": "Only you and other owners of the repository can see this project." + }, + "internal": { + "internal": "Internal", + "desc": "Only authenticated users of the Woodpecker instance can see this project." + } + }, "settings": { "not_allowed": "You are not allowed to access this repository's settings", "general": { @@ -107,21 +122,6 @@ "desc": "Underlying pipeline containers get access to security privileges." } }, - "visibility": { - "visibility": "Project visibility", - "public": { - "public": "Public", - "desc": "Every user can see your project without being logged in." - }, - "private": { - "private": "Private", - "desc": "Only you and other owners of the repository can see this project." - }, - "internal": { - "internal": "Internal", - "desc": "Only authenticated users of the Woodpecker instance can see this project." - } - }, "timeout": { "timeout": "Timeout", "minutes": "minutes" @@ -509,5 +509,7 @@ "pull_requests": "All pull requests", "all_events": "All events from forge", "desc": "Prevent malicious pipelines from exposing secrets or running harmful tasks by approving them before execution." - } + }, + "all_repositories": "All repositories", + "no_search_results": "No results found" } diff --git a/web/src/components/repo/RepoItem.vue b/web/src/components/repo/RepoItem.vue new file mode 100644 index 000000000..84346aecd --- /dev/null +++ b/web/src/components/repo/RepoItem.vue @@ -0,0 +1,50 @@ + + + diff --git a/web/src/components/repo/pipeline/PipelineLog.vue b/web/src/components/repo/pipeline/PipelineLog.vue index 5fa128cb6..124d7ada3 100644 --- a/web/src/components/repo/pipeline/PipelineLog.vue +++ b/web/src/components/repo/pipeline/PipelineLog.vue @@ -163,7 +163,7 @@ const hasLogs = computed( // we do not have logs for skipped steps repo?.value && pipeline.value && step.value && step.value.state !== 'skipped', ); -const autoScroll = useStorage('log-auto-scroll', false); +const autoScroll = useStorage('woodpecker:log-auto-scroll', false); const showActions = ref(false); const downloadInProgress = ref(false); const ansiUp = ref(new AnsiUp()); diff --git a/web/src/compositions/useRepos.ts b/web/src/compositions/useRepos.ts new file mode 100644 index 000000000..d094a847b --- /dev/null +++ b/web/src/compositions/useRepos.ts @@ -0,0 +1,55 @@ +import { useStorage } from '@vueuse/core'; +import { ref } from 'vue'; + +import type { Repo } from '~/lib/api/types'; +import { usePipelineStore } from '~/store/pipelines'; + +export default function useRepos() { + const pipelineStore = usePipelineStore(); + const lastAccess = useStorage('woodpecker:repo-last-access', new Map()); + + function repoWithLastPipeline(repo: Repo): Repo { + if (repo.last_pipeline === undefined) { + return repo; + } + + if (repo.last_pipeline_item?.number === repo.last_pipeline) { + return repo; + } + + const lastPipeline = pipelineStore.getPipeline(ref(repo.id), ref(repo.last_pipeline)).value; + + return { + ...repo, + last_pipeline_item: lastPipeline, + }; + } + + function sortReposByLastAccess(repos: Repo[]): Repo[] { + return repos.sort((a, b) => { + const aLastAccess = lastAccess.value.get(a.id) ?? 0; + const bLastAccess = lastAccess.value.get(b.id) ?? 0; + + return bLastAccess - aLastAccess; + }); + } + + function sortReposByLastActivity(repos: Repo[]): Repo[] { + return repos.sort((a, b) => { + const aLastActivity = a.last_pipeline_item?.created ?? 0; + const bLastActivity = b.last_pipeline_item?.created ?? 0; + return bLastActivity - aLastActivity; + }); + } + + function updateLastAccess(repoId: number) { + lastAccess.value.set(repoId, Date.now()); + } + + return { + sortReposByLastAccess, + sortReposByLastActivity, + repoWithLastPipeline, + updateLastAccess, + }; +} diff --git a/web/src/compositions/useUserConfig.ts b/web/src/compositions/useUserConfig.ts index 294195273..e9019aa54 100644 --- a/web/src/compositions/useUserConfig.ts +++ b/web/src/compositions/useUserConfig.ts @@ -1,32 +1,19 @@ -import { computed, ref } from 'vue'; - -const USER_CONFIG_KEY = 'woodpecker-user-config'; +import { useStorage } from '@vueuse/core'; +import { computed } from 'vue'; interface UserConfig { isPipelineFeedOpen: boolean; redirectUrl: string; } -const defaultUserConfig: UserConfig = { +const config = useStorage('woodpecker:user-config', { isPipelineFeedOpen: false, redirectUrl: '', -}; - -function loadUserConfig(): UserConfig { - const lsData = localStorage.getItem(USER_CONFIG_KEY); - if (lsData === null) { - return defaultUserConfig; - } - - return JSON.parse(lsData) as UserConfig; -} - -const config = ref(loadUserConfig()); +}); export default () => ({ setUserConfig(key: T, value: UserConfig[T]): void { config.value = { ...config.value, [key]: value }; - localStorage.setItem(USER_CONFIG_KEY, JSON.stringify(config.value)); }, userConfig: computed(() => config.value), }); diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index 12f5c5f59..3062279c0 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -1,3 +1,5 @@ +import type { Pipeline } from './pipeline'; + // A version control repository. export interface Repo { // Is the repo currently active or not @@ -65,7 +67,9 @@ export interface Repo { visibility: RepoVisibility; - last_pipeline: number; + last_pipeline?: number; + + last_pipeline_item?: Pipeline; require_approval: RepoRequireApproval; diff --git a/web/src/store/pipelines.ts b/web/src/store/pipelines.ts index c8d66871e..22a80500b 100644 --- a/web/src/store/pipelines.ts +++ b/web/src/store/pipelines.ts @@ -18,6 +18,14 @@ export const usePipelineStore = defineStore('pipelines', () => { ...(repoPipelines.get(pipeline.number) || {}), ...pipeline, }); + + // Update last pipeline number for the repo + const repo = repoStore.repos.get(repoId); + if (repo?.last_pipeline !== undefined && repo.last_pipeline < pipeline.number) { + repo.last_pipeline = pipeline.number; + repoStore.setRepo(repo); + } + pipelines.set(repoId, repoPipelines); } @@ -25,10 +33,14 @@ export const usePipelineStore = defineStore('pipelines', () => { return computed(() => Array.from(pipelines.get(repoId.value)?.values() || []).sort(comparePipelines)); } - function getPipeline(repoId: Ref, _pipelineNumber: Ref) { + function getPipeline(repoId: Ref, _pipelineNumber: Ref) { return computed(() => { - const pipelineNumber = Number.parseInt(_pipelineNumber.value, 10); - return pipelines.get(repoId.value)?.get(pipelineNumber); + if (typeof _pipelineNumber.value === 'string') { + const pipelineNumber = Number.parseInt(_pipelineNumber.value, 10); + return pipelines.get(repoId.value)?.get(pipelineNumber); + } + + return pipelines.get(repoId.value)?.get(_pipelineNumber.value); }); } diff --git a/web/src/store/repos.ts b/web/src/store/repos.ts index a60afc5fa..cf767d04b 100644 --- a/web/src/store/repos.ts +++ b/web/src/store/repos.ts @@ -4,8 +4,11 @@ import { computed, reactive, ref, type Ref } from 'vue'; import useApiClient from '~/compositions/useApiClient'; import type { Repo } from '~/lib/api/types'; +import { usePipelineStore } from './pipelines'; + export const useRepoStore = defineStore('repos', () => { const apiClient = useApiClient(); + const pipelineStore = usePipelineStore(); const repos: Map = reactive(new Map()); const ownedRepoIds = ref([]); @@ -21,20 +24,30 @@ export const useRepoStore = defineStore('repos', () => { } function setRepo(repo: Repo) { - repos.set(repo.id, repo); + repos.set(repo.id, { + ...repos.get(repo.id), + ...repo, + }); } async function loadRepo(repoId: number) { const repo = await apiClient.getRepo(repoId); - repos.set(repo.id, repo); + setRepo(repo); return repo; } async function loadRepos() { const _ownedRepos = await apiClient.getRepoList(); - _ownedRepos.forEach((repo) => { - repos.set(repo.id, repo); - }); + await Promise.all( + _ownedRepos.map(async (repo) => { + const lastPipeline = await apiClient.getPipelineList(repo.id, { page: 1, perPage: 1 }); + if (lastPipeline.length === 1) { + pipelineStore.setPipeline(repo.id, lastPipeline[0]); + repo.last_pipeline = lastPipeline[0].number; + } + setRepo(repo); + }), + ); ownedRepoIds.value = _ownedRepos.map((repo) => repo.id); } diff --git a/web/src/utils/locale.ts b/web/src/utils/locale.ts index 091a85bde..2e68c8311 100644 --- a/web/src/utils/locale.ts +++ b/web/src/utils/locale.ts @@ -1,8 +1,8 @@ -import { useLocalStorage } from '@vueuse/core'; +import { useStorage } from '@vueuse/core'; export function getUserLanguage(): string { const browserLocale = navigator.language.split('-')[0]; - const selectedLocale = useLocalStorage('woodpecker:locale', browserLocale).value; + const selectedLocale = useStorage('woodpecker:locale', browserLocale).value; return selectedLocale; } diff --git a/web/src/views/Repos.vue b/web/src/views/Repos.vue index f70ea1941..503d3cbb0 100644 --- a/web/src/views/Repos.vue +++ b/web/src/views/Repos.vue @@ -8,11 +8,26 @@