mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-01 20:22:21 +00:00
Add repos list for admins (#2347)
This commit is contained in:
parent
d7000e06e0
commit
d04bb72e5e
11 changed files with 233 additions and 50 deletions
|
@ -1350,6 +1350,56 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/repos": {
|
"/repos": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"summary": "List all repositories on the server. Requires admin rights.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer \u003cpersonal access token\u003e",
|
||||||
|
"description": "Insert your personal access token",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "only list active repos",
|
||||||
|
"name": "active",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1,
|
||||||
|
"description": "for response pagination, page offset number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 50,
|
||||||
|
"description": "for response pagination, max items per page",
|
||||||
|
"name": "perPage",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Repo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
|
|
@ -550,3 +550,28 @@ func MoveRepo(c *gin.Context) {
|
||||||
}
|
}
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllRepos
|
||||||
|
//
|
||||||
|
// @Summary List all repositories on the server. Requires admin rights.
|
||||||
|
// @Router /repos [get]
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} Repo
|
||||||
|
// @Tags Repositories
|
||||||
|
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||||
|
// @Param active query bool false "only list active repos"
|
||||||
|
// @Param page query int false "for response pagination, page offset number" default(1)
|
||||||
|
// @Param perPage query int false "for response pagination, max items per page" default(50)
|
||||||
|
func GetAllRepos(c *gin.Context) {
|
||||||
|
_store := store.FromContext(c)
|
||||||
|
|
||||||
|
active, _ := strconv.ParseBool(c.Query("active"))
|
||||||
|
|
||||||
|
repos, err := _store.RepoListAll(active, session.Pagination(c))
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching repository list. %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, repos)
|
||||||
|
}
|
||||||
|
|
|
@ -68,9 +68,12 @@ func apiRoutes(e *gin.RouterGroup) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiBase.GET("/repos/lookup/*repo_full_name", session.SetRepo(), session.SetPerm(), session.MustPull, api.LookupRepo)
|
repo := apiBase.Group("/repos")
|
||||||
apiBase.POST("/repos", session.MustUser(), api.PostRepo)
|
{
|
||||||
repoBase := apiBase.Group("/repos/:repo_id")
|
repo.GET("/lookup/*repo_full_name", session.SetRepo(), session.SetPerm(), session.MustPull, api.LookupRepo)
|
||||||
|
repo.POST("", session.MustUser(), api.PostRepo)
|
||||||
|
repo.GET("", session.MustAdmin(), api.GetAllRepos)
|
||||||
|
repoBase := repo.Group("/:repo_id")
|
||||||
{
|
{
|
||||||
repoBase.Use(session.SetRepo())
|
repoBase.Use(session.SetRepo())
|
||||||
repoBase.Use(session.SetPerm())
|
repoBase.Use(session.SetPerm())
|
||||||
|
@ -132,6 +135,7 @@ func apiRoutes(e *gin.RouterGroup) {
|
||||||
repo.POST("/move", session.MustRepoAdmin(), api.MoveRepo)
|
repo.POST("/move", session.MustRepoAdmin(), api.MoveRepo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
badges := apiBase.Group("/badges/:repo_id_or_owner")
|
badges := apiBase.Group("/badges/:repo_id_or_owner")
|
||||||
{
|
{
|
||||||
|
|
|
@ -149,3 +149,15 @@ func (s storage) RepoList(user *model.User, owned, active bool) ([]*model.Repo,
|
||||||
Asc("repo_full_name").
|
Asc("repo_full_name").
|
||||||
Find(&repos)
|
Find(&repos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepoListAll list all repos
|
||||||
|
func (s storage) RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error) {
|
||||||
|
repos := make([]*model.Repo, 0)
|
||||||
|
sess := s.paginate(p).Table("repos")
|
||||||
|
if active {
|
||||||
|
sess = sess.And(builder.Eq{"repos.repo_active": true})
|
||||||
|
}
|
||||||
|
return repos, sess.
|
||||||
|
Asc("repo_full_name").
|
||||||
|
Find(&repos)
|
||||||
|
}
|
||||||
|
|
|
@ -1583,6 +1583,32 @@ func (_m *Store) RepoList(user *model.User, owned bool, active bool) ([]*model.R
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepoListAll provides a mock function with given fields: active, p
|
||||||
|
func (_m *Store) RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error) {
|
||||||
|
ret := _m.Called(active, p)
|
||||||
|
|
||||||
|
var r0 []*model.Repo
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(bool, *model.ListOptions) ([]*model.Repo, error)); ok {
|
||||||
|
return rf(active, p)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(bool, *model.ListOptions) []*model.Repo); ok {
|
||||||
|
r0 = rf(active, p)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.Repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(bool, *model.ListOptions) error); ok {
|
||||||
|
r1 = rf(active, p)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// RepoListLatest provides a mock function with given fields: _a0
|
// RepoListLatest provides a mock function with given fields: _a0
|
||||||
func (_m *Store) RepoListLatest(_a0 *model.User) ([]*model.Feed, error) {
|
func (_m *Store) RepoListLatest(_a0 *model.User) ([]*model.Feed, error) {
|
||||||
ret := _m.Called(_a0)
|
ret := _m.Called(_a0)
|
||||||
|
|
|
@ -100,6 +100,7 @@ type Store interface {
|
||||||
// Repositories
|
// Repositories
|
||||||
RepoList(user *model.User, owned, active bool) ([]*model.Repo, error)
|
RepoList(user *model.User, owned, active bool) ([]*model.Repo, error)
|
||||||
RepoListLatest(*model.User) ([]*model.Feed, error)
|
RepoListLatest(*model.User) ([]*model.Feed, error)
|
||||||
|
RepoListAll(active bool, p *model.ListOptions) ([]*model.Repo, error)
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
PermFind(user *model.User, repo *model.Repo) (*model.Perm, error)
|
PermFind(user *model.User, repo *model.Repo) (*model.Perm, error)
|
||||||
|
|
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
|
@ -15,6 +15,7 @@ declare module '@vue/runtime-core' {
|
||||||
AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default']
|
AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default']
|
||||||
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
|
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
|
||||||
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
||||||
|
AdminReposTab: typeof import('./src/components/admin/settings/AdminReposTab.vue')['default']
|
||||||
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
||||||
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
|
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
|
||||||
Badge: typeof import('./src/components/atomic/Badge.vue')['default']
|
Badge: typeof import('./src/components/atomic/Badge.vue')['default']
|
||||||
|
|
|
@ -432,6 +432,14 @@
|
||||||
"deleted": "Organization deleted",
|
"deleted": "Organization deleted",
|
||||||
"delete_confirm": "Do you really want to delete this organization? This will also delete all repositories owned by this organization.",
|
"delete_confirm": "Do you really want to delete this organization? This will also delete all repositories owned by this organization.",
|
||||||
"view": "View organization"
|
"view": "View organization"
|
||||||
|
},
|
||||||
|
"repos": {
|
||||||
|
"repos": "Repositories",
|
||||||
|
"desc": "Repositories that are or were enabled on this server",
|
||||||
|
"none": "There are no repositories yet.",
|
||||||
|
"view": "View repository",
|
||||||
|
"settings": "Repository settings",
|
||||||
|
"disabled": "Disabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
48
web/src/components/admin/settings/AdminReposTab.vue
Normal file
48
web/src/components/admin/settings/AdminReposTab.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<Settings :title="$t('admin.settings.repos.repos')" :desc="$t('admin.settings.repos.desc')">
|
||||||
|
<div class="space-y-4 text-wp-text-100">
|
||||||
|
<ListItem
|
||||||
|
v-for="repo in repos"
|
||||||
|
:key="repo.id"
|
||||||
|
class="items-center gap-2 !bg-wp-background-200 !dark:bg-wp-background-100"
|
||||||
|
>
|
||||||
|
<span>{{ repo.full_name }}</span>
|
||||||
|
<div class="ml-auto flex items-center">
|
||||||
|
<Badge v-if="!repo.active" class="<md:hidden mr-2" :label="$t('admin.settings.repos.disabled')" />
|
||||||
|
<IconButton
|
||||||
|
icon="chevron-right"
|
||||||
|
:title="$t('admin.settings.repos.view')"
|
||||||
|
class="w-8 h-8"
|
||||||
|
:to="{ name: 'repo', params: { repoId: repo.id } }"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="settings"
|
||||||
|
:title="$t('admin.settings.repos.settings')"
|
||||||
|
class="w-8 h-8"
|
||||||
|
:to="{ name: 'repo-settings', params: { repoId: repo.id } }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<div v-if="repos?.length === 0" class="ml-2">{{ $t('admin.settings.orgs.none') }}</div>
|
||||||
|
</div>
|
||||||
|
</Settings>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Badge from '~/components/atomic/Badge.vue';
|
||||||
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
|
import ListItem from '~/components/atomic/ListItem.vue';
|
||||||
|
import Settings from '~/components/layout/Settings.vue';
|
||||||
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import { usePagination } from '~/compositions/usePaginate';
|
||||||
|
import { Repo } from '~/lib/api/types';
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
|
||||||
|
async function loadRepos(page: number): Promise<Repo[] | null> {
|
||||||
|
return apiClient.getAllRepos(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: repos } = usePagination(loadRepos);
|
||||||
|
</script>
|
|
@ -311,6 +311,10 @@ export default class WoodpeckerClient extends ApiClient {
|
||||||
return this._delete(`/api/orgs/${org.id}`);
|
return this._delete(`/api/orgs/${org.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllRepos(page: number): Promise<Repo[] | null> {
|
||||||
|
return this._get(`/api/repos?page=${page}`) as Promise<Repo[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
||||||
on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource {
|
on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource {
|
||||||
return this._subscribe('/api/stream/events', callback, {
|
return this._subscribe('/api/stream/events', callback, {
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
|
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
|
||||||
<AdminSecretsTab />
|
<AdminSecretsTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab id="repos" :title="$t('admin.settings.repos.repos')">
|
||||||
|
<AdminReposTab />
|
||||||
|
</Tab>
|
||||||
<Tab id="users" :title="$t('admin.settings.users.users')">
|
<Tab id="users" :title="$t('admin.settings.users.users')">
|
||||||
<AdminUsersTab />
|
<AdminUsersTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -29,6 +32,7 @@ import { useRouter } from 'vue-router';
|
||||||
import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
|
import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
|
||||||
import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue';
|
import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue';
|
||||||
import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
|
import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
|
||||||
|
import AdminReposTab from '~/components/admin/settings/AdminReposTab.vue';
|
||||||
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
|
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
|
||||||
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';
|
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';
|
||||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
|
|
Loading…
Reference in a new issue