Compare commits
2 Commits
cc176b4d29
...
ac9ac4cbc7
| Author | SHA1 | Date | |
|---|---|---|---|
| ac9ac4cbc7 | |||
| 8855bb77fb |
@@ -11,6 +11,7 @@ import "strings"
|
||||
import "codit/internal/auth"
|
||||
import "codit/internal/config"
|
||||
import "codit/internal/db"
|
||||
import "codit/internal/docker"
|
||||
import "codit/internal/git"
|
||||
import "codit/internal/handlers"
|
||||
import httpx "codit/internal/http"
|
||||
@@ -246,7 +247,9 @@ func main() {
|
||||
var repoManager git.RepoManager
|
||||
repoManager = git.RepoManager{BaseDir: filepath.Join(cfg.DataDir, "git")}
|
||||
var rpmBase string
|
||||
var dockerBase string
|
||||
rpmBase = filepath.Join(cfg.DataDir, "rpm")
|
||||
dockerBase = filepath.Join(cfg.DataDir, "docker")
|
||||
var rpmMeta *rpm.MetaManager
|
||||
rpmMeta = rpm.NewMetaManager()
|
||||
var uploadStore storage.FileStore
|
||||
@@ -255,6 +258,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("rpm dir error: %v", err)
|
||||
}
|
||||
err = os.MkdirAll(dockerBase, 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("docker dir error: %v", err)
|
||||
}
|
||||
|
||||
var api *handlers.API
|
||||
api = &handlers.API{
|
||||
@@ -263,6 +270,7 @@ func main() {
|
||||
Repos: repoManager,
|
||||
RpmBase: rpmBase,
|
||||
RpmMeta: rpmMeta,
|
||||
DockerBase: dockerBase,
|
||||
Uploads: uploadStore,
|
||||
}
|
||||
|
||||
@@ -294,6 +302,8 @@ func main() {
|
||||
}
|
||||
var rpmServer http.Handler
|
||||
rpmServer = rpm.NewHTTPServer(rpmBase, authFunc, logger)
|
||||
var dockerServer http.Handler
|
||||
dockerServer = docker.NewHTTPServer(store, dockerBase, authFunc, logger)
|
||||
|
||||
var router *httpx.Router
|
||||
router = httpx.NewRouter()
|
||||
@@ -353,6 +363,11 @@ func main() {
|
||||
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
||||
router.Handle("GET", "/api/repos/:id/rpm/tree", api.RepoRPMTree)
|
||||
router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
|
||||
router.Handle("GET", "/api/repos/:id/docker/images", api.RepoDockerImages)
|
||||
router.Handle("GET", "/api/repos/:id/docker/tags", api.RepoDockerTags)
|
||||
router.Handle("GET", "/api/repos/:id/docker/manifest", api.RepoDockerManifest)
|
||||
router.Handle("DELETE", "/api/repos/:id/docker/tag", api.RepoDockerDeleteTag)
|
||||
router.Handle("DELETE", "/api/repos/:id/docker/image", api.RepoDockerDeleteImage)
|
||||
|
||||
router.Handle("GET", "/api/projects/:projectId/issues", api.ListIssues)
|
||||
router.Handle("POST", "/api/projects/:projectId/issues", api.CreateIssue)
|
||||
@@ -377,6 +392,8 @@ func main() {
|
||||
mux.Handle(cfg.RPMHTTPPrefix+"/", http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite))
|
||||
rpmIDRewrite = &rpmIDPathRewriteHandler{next: rpmServer, store: store}
|
||||
mux.Handle(cfg.RPMHTTPPrefix+"-id/", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite))
|
||||
mux.Handle("/v2", dockerServer)
|
||||
mux.Handle("/v2/", dockerServer)
|
||||
mux.Handle("/api/graphql", middleware.WithUser(store, middleware.RequireAuth(graphqlHandler)))
|
||||
mux.Handle("/api/", middleware.WithUser(store, middleware.AccessLog(logger, middleware.RequireAuth(router))))
|
||||
mux.Handle("/api/login", middleware.WithUser(store, middleware.AccessLog(logger, router)))
|
||||
|
||||
@@ -14,6 +14,7 @@ import "unicode"
|
||||
import "codit/internal/auth"
|
||||
import "codit/internal/config"
|
||||
import "codit/internal/db"
|
||||
import "codit/internal/docker"
|
||||
import "codit/internal/git"
|
||||
import "codit/internal/middleware"
|
||||
import "codit/internal/models"
|
||||
@@ -27,6 +28,7 @@ type API struct {
|
||||
Repos git.RepoManager
|
||||
RpmBase string
|
||||
RpmMeta *rpm.MetaManager
|
||||
DockerBase string
|
||||
Uploads storage.FileStore
|
||||
}
|
||||
|
||||
@@ -66,12 +68,14 @@ type repoResponse struct {
|
||||
models.Repo
|
||||
CloneURL string `json:"clone_url"`
|
||||
RPMURL string `json:"rpm_url"`
|
||||
DockerURL string `json:"docker_url"`
|
||||
}
|
||||
|
||||
type repoListItem struct {
|
||||
models.Repo
|
||||
CloneURL string `json:"clone_url"`
|
||||
RPMURL string `json:"rpm_url"`
|
||||
DockerURL string `json:"docker_url"`
|
||||
OwnerProject string `json:"owner_project"`
|
||||
OwnerSlug string `json:"owner_slug"`
|
||||
}
|
||||
@@ -646,16 +650,21 @@ func (api *API) ListRepos(w http.ResponseWriter, r *http.Request, params map[str
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
||||
}
|
||||
resp = append(resp, repoListItem{
|
||||
Repo: repo,
|
||||
CloneURL: cloneURL,
|
||||
RPMURL: rpmURL,
|
||||
DockerURL: dockerURL,
|
||||
OwnerProject: owner.ID,
|
||||
OwnerSlug: owner.Slug,
|
||||
})
|
||||
@@ -728,16 +737,21 @@ func (api *API) ListAllRepos(w http.ResponseWriter, r *http.Request, _ map[strin
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(owner.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(owner.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(owner.Slug, repo.Name)
|
||||
}
|
||||
resp = append(resp, repoListItem{
|
||||
Repo: repo,
|
||||
CloneURL: cloneURL,
|
||||
RPMURL: rpmURL,
|
||||
DockerURL: dockerURL,
|
||||
OwnerProject: owner.ID,
|
||||
OwnerSlug: owner.Slug,
|
||||
})
|
||||
@@ -764,13 +778,17 @@ func (api *API) GetRepo(w http.ResponseWriter, r *http.Request, params map[strin
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||
}
|
||||
|
||||
func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
@@ -833,13 +851,20 @@ func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
} else if repoType == "rpm" {
|
||||
repoPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
||||
err = os.MkdirAll(repoPath, 0o755)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if repoType == "docker" {
|
||||
repoPath = filepath.Join(api.DockerBase, project.ID, req.Name)
|
||||
err = os.MkdirAll(repoPath, 0o755)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
repo = models.Repo{
|
||||
ID: repoID,
|
||||
@@ -857,13 +882,17 @@ func (api *API) CreateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if created.Type == "git" {
|
||||
cloneURL = api.cloneURL(project.Slug, created.Name)
|
||||
}
|
||||
if created.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(project.Slug, created.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL})
|
||||
if created.Type == "docker" {
|
||||
dockerURL = api.dockerURL(project.Slug, created.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusCreated, repoResponse{Repo: created, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||
}
|
||||
|
||||
func (api *API) ListAvailableRepos(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
@@ -963,9 +992,23 @@ func (api *API) AttachForeignRepo(w http.ResponseWriter, r *http.Request, params
|
||||
return
|
||||
}
|
||||
repo.IsForeign = true
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusCreated, repoListItem{
|
||||
Repo: repo,
|
||||
CloneURL: api.cloneURL(project.Slug, repo.Name),
|
||||
CloneURL: cloneURL,
|
||||
RPMURL: rpmURL,
|
||||
DockerURL: dockerURL,
|
||||
OwnerProject: project.ID,
|
||||
OwnerSlug: project.Slug,
|
||||
})
|
||||
@@ -1031,8 +1074,10 @@ func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
||||
}
|
||||
if repo.Type == "git" {
|
||||
newPath = api.Repos.RepoPathFor(project.ID, req.Name)
|
||||
} else {
|
||||
} else if repo.Type == "rpm" {
|
||||
newPath = filepath.Join(api.RpmBase, project.ID, req.Name)
|
||||
} else if repo.Type == "docker" {
|
||||
newPath = filepath.Join(api.DockerBase, project.ID, req.Name)
|
||||
}
|
||||
if repo.Path != newPath {
|
||||
oldPath = repo.Path
|
||||
@@ -1069,13 +1114,17 @@ func (api *API) UpdateRepo(w http.ResponseWriter, r *http.Request, params map[st
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||
}
|
||||
|
||||
func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
@@ -1116,13 +1165,17 @@ func (api *API) DeleteRepo(w http.ResponseWriter, r *http.Request, params map[st
|
||||
}
|
||||
var cloneURL string
|
||||
var rpmURL string
|
||||
var dockerURL string
|
||||
if repo.Type == "git" {
|
||||
cloneURL = api.cloneURL(project.Slug, repo.Name)
|
||||
}
|
||||
if repo.Type == "rpm" {
|
||||
rpmURL = api.rpmURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL})
|
||||
if repo.Type == "docker" {
|
||||
dockerURL = api.dockerURL(project.Slug, repo.Name)
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, repoResponse{Repo: repo, CloneURL: cloneURL, RPMURL: rpmURL, DockerURL: dockerURL})
|
||||
}
|
||||
|
||||
func moveToTrash(path string) (string, error) {
|
||||
@@ -1774,6 +1827,7 @@ func (api *API) RepoTypes(w http.ResponseWriter, r *http.Request, _ map[string]s
|
||||
types = []repoTypeItem{
|
||||
{Value: "git", Label: "Git"},
|
||||
{Value: "rpm", Label: "RPM"},
|
||||
{Value: "docker", Label: "Docker"},
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, types)
|
||||
}
|
||||
@@ -2357,6 +2411,154 @@ func (api *API) RepoRPMUpload(w http.ResponseWriter, r *http.Request, params map
|
||||
WriteJSON(w, http.StatusOK, repoRPMUploadResponse{Filename: filename, Size: size})
|
||||
}
|
||||
|
||||
func (api *API) RepoDockerTags(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var repo models.Repo
|
||||
var err error
|
||||
var tags []docker.TagInfo
|
||||
var image string
|
||||
var imagePath string
|
||||
repo, err = api.Store.GetRepo(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||
return
|
||||
}
|
||||
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||
return
|
||||
}
|
||||
if repo.Type != "docker" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||
return
|
||||
}
|
||||
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||
imagePath = docker.ImagePath(repo.Path, image)
|
||||
tags, err = docker.ListTags(imagePath)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, tags)
|
||||
}
|
||||
|
||||
func (api *API) RepoDockerManifest(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var repo models.Repo
|
||||
var err error
|
||||
var ref string
|
||||
var detail docker.ManifestDetail
|
||||
var image string
|
||||
var imagePath string
|
||||
repo, err = api.Store.GetRepo(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||
return
|
||||
}
|
||||
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||
return
|
||||
}
|
||||
if repo.Type != "docker" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||
return
|
||||
}
|
||||
ref = strings.TrimSpace(r.URL.Query().Get("ref"))
|
||||
if ref == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "ref required"})
|
||||
return
|
||||
}
|
||||
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||
imagePath = docker.ImagePath(repo.Path, image)
|
||||
detail, err = docker.GetManifestDetail(imagePath, ref)
|
||||
if err != nil {
|
||||
if err == docker.ErrNotFound {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "manifest not found"})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
func (api *API) RepoDockerImages(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var repo models.Repo
|
||||
var err error
|
||||
var images []string
|
||||
repo, err = api.Store.GetRepo(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||
return
|
||||
}
|
||||
if !api.requireRepoRole(w, r, repo.ID, "viewer") {
|
||||
return
|
||||
}
|
||||
if repo.Type != "docker" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||
return
|
||||
}
|
||||
images, err = docker.ListImages(repo.Path)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, images)
|
||||
}
|
||||
|
||||
func (api *API) RepoDockerDeleteTag(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var repo models.Repo
|
||||
var err error
|
||||
var image string
|
||||
var tag string
|
||||
var imagePath string
|
||||
repo, err = api.Store.GetRepo(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||
return
|
||||
}
|
||||
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
||||
return
|
||||
}
|
||||
if repo.Type != "docker" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||
return
|
||||
}
|
||||
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||
tag = strings.TrimSpace(r.URL.Query().Get("tag"))
|
||||
if tag == "" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "tag required"})
|
||||
return
|
||||
}
|
||||
imagePath = docker.ImagePath(repo.Path, image)
|
||||
err = docker.DeleteTag(imagePath, tag)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (api *API) RepoDockerDeleteImage(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var repo models.Repo
|
||||
var err error
|
||||
var image string
|
||||
repo, err = api.Store.GetRepo(params["id"])
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "repo not found"})
|
||||
return
|
||||
}
|
||||
if !api.requireRepoRole(w, r, repo.ID, "writer") {
|
||||
return
|
||||
}
|
||||
if repo.Type != "docker" {
|
||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "repo is not docker"})
|
||||
return
|
||||
}
|
||||
image = strings.TrimSpace(r.URL.Query().Get("image"))
|
||||
err = docker.DeleteImage(repo.Path, image)
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (api *API) ListIssues(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var issues []models.Issue
|
||||
var err error
|
||||
@@ -2737,7 +2939,7 @@ func normalizeRepoType(value string) (string, bool) {
|
||||
if v == "" {
|
||||
return "git", true
|
||||
}
|
||||
if v == "git" || v == "rpm" {
|
||||
if v == "git" || v == "rpm" || v == "docker" {
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
@@ -2916,3 +3118,9 @@ func (api *API) rpmURL(projectSlug, repoName string) string {
|
||||
}
|
||||
return base + prefix + "/" + projectSlug + "/" + repoName
|
||||
}
|
||||
|
||||
func (api *API) dockerURL(projectSlug, repoName string) string {
|
||||
var base string
|
||||
base = strings.TrimRight(api.Cfg.PublicBaseURL, "/")
|
||||
return base + "/v2/" + projectSlug + "/" + repoName
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ export interface Repo {
|
||||
id: string
|
||||
project_id: string
|
||||
name: string
|
||||
type?: 'git' | 'rpm'
|
||||
type?: 'git' | 'rpm' | 'docker'
|
||||
clone_url?: string
|
||||
rpm_url?: string
|
||||
docker_url?: string
|
||||
is_foreign?: boolean
|
||||
owner_project?: string
|
||||
owner_slug?: string
|
||||
@@ -64,6 +65,30 @@ export interface RpmPackageDetail extends RpmPackageSummary {
|
||||
provides: string[]
|
||||
}
|
||||
|
||||
export interface DockerTagInfo {
|
||||
tag: string
|
||||
digest: string
|
||||
size: number
|
||||
media_type: string
|
||||
}
|
||||
|
||||
export interface DockerManifestDetail {
|
||||
reference: string
|
||||
digest: string
|
||||
media_type: string
|
||||
size: number
|
||||
config: {
|
||||
created?: string
|
||||
architecture?: string
|
||||
os?: string
|
||||
}
|
||||
layers: {
|
||||
mediaType: string
|
||||
digest: string
|
||||
size: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface RpmTreeEntry {
|
||||
name: string
|
||||
path: string
|
||||
@@ -375,6 +400,29 @@ export const api = {
|
||||
params.set('path', path)
|
||||
return requestBinary(`/api/repos/${repoId}/rpm/file?${params.toString()}`)
|
||||
},
|
||||
listDockerImages: (repoId: string) => request<string[]>(`/api/repos/${repoId}/docker/images`),
|
||||
listDockerTags: (repoId: string, image: string) => {
|
||||
const params = new URLSearchParams()
|
||||
if (image) params.set('image', image)
|
||||
return request<DockerTagInfo[]>(`/api/repos/${repoId}/docker/tags?${params.toString()}`)
|
||||
},
|
||||
getDockerManifest: (repoId: string, ref: string, image: string) => {
|
||||
const params = new URLSearchParams()
|
||||
params.set('ref', ref)
|
||||
if (image) params.set('image', image)
|
||||
return request<DockerManifestDetail>(`/api/repos/${repoId}/docker/manifest?${params.toString()}`)
|
||||
},
|
||||
deleteDockerTag: (repoId: string, image: string, tag: string) => {
|
||||
const params = new URLSearchParams()
|
||||
if (image) params.set('image', image)
|
||||
params.set('tag', tag)
|
||||
return request<{ status: string }>(`/api/repos/${repoId}/docker/tag?${params.toString()}`, { method: 'DELETE' })
|
||||
},
|
||||
deleteDockerImage: (repoId: string, image: string) => {
|
||||
const params = new URLSearchParams()
|
||||
if (image) params.set('image', image)
|
||||
return request<{ status: string }>(`/api/repos/${repoId}/docker/image?${params.toString()}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
listIssues: (projectId: string) => request<Issue[]>(`/api/projects/${projectId}/issues`),
|
||||
createIssue: (projectId: string, payload: { title: string; body: string }) =>
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function RepoSubNav(props: RepoSubNavProps) {
|
||||
const isCommits = currentPath.startsWith(`${basePath}/commits`)
|
||||
const isBranches = currentPath.startsWith(`${basePath}/branches`)
|
||||
const isRPM = repoType === 'rpm'
|
||||
const isDocker = repoType === 'docker'
|
||||
|
||||
return (
|
||||
<Paper
|
||||
@@ -35,9 +36,9 @@ export default function RepoSubNav(props: RepoSubNavProps) {
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button component={Link} to={codePath} variant={isCode ? 'contained' : 'outlined'} size="small">
|
||||
{isRPM ? 'Packages' : 'Code'}
|
||||
{isRPM ? 'Packages' : isDocker ? 'Images' : 'Code'}
|
||||
</Button>
|
||||
{!isRPM ? (
|
||||
{!isRPM && !isDocker ? (
|
||||
<>
|
||||
<Button component={Link} to={commitsPath} variant={isCommits ? 'contained' : 'outlined'} size="small">
|
||||
Commits
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function GlobalReposPage() {
|
||||
const [reposError, setReposError] = useState<string | null>(null)
|
||||
const [reposLoading, setReposLoading] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | 'git' | 'rpm'>('all')
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | 'git' | 'rpm' | 'docker'>('all')
|
||||
const [page, setPage] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [pageSizeInput, setPageSizeInput] = useState('20')
|
||||
@@ -82,7 +82,7 @@ export default function GlobalReposPage() {
|
||||
size="small"
|
||||
value={typeFilter}
|
||||
onChange={(event) => {
|
||||
setTypeFilter(event.target.value as 'all' | 'git' | 'rpm')
|
||||
setTypeFilter(event.target.value as 'all' | 'git' | 'rpm' | 'docker')
|
||||
setPage(0)
|
||||
}}
|
||||
sx={{ minWidth: 120 }}
|
||||
@@ -90,6 +90,7 @@ export default function GlobalReposPage() {
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
<MenuItem value="git">Git</MenuItem>
|
||||
<MenuItem value="rpm">RPM</MenuItem>
|
||||
<MenuItem value="docker">Docker</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Items"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { api, Repo } from '../api'
|
||||
import RepoGitDetailPage from './RepoGitDetailPage'
|
||||
import RepoDockerDetailPage from './RepoDockerDetailPage'
|
||||
import RepoRpmDetailPage from './RepoRpmDetailPage'
|
||||
|
||||
export default function RepoDetailPage() {
|
||||
@@ -69,5 +70,8 @@ export default function RepoDetailPage() {
|
||||
if (repo.type === 'rpm') {
|
||||
return <RepoRpmDetailPage initialRepo={repo} />
|
||||
}
|
||||
if (repo.type === 'docker') {
|
||||
return <RepoDockerDetailPage initialRepo={repo} />
|
||||
}
|
||||
return <RepoGitDetailPage initialRepo={repo} />
|
||||
}
|
||||
|
||||
460
frontend/src/pages/RepoDockerDetailPage.tsx
Normal file
460
frontend/src/pages/RepoDockerDetailPage.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { api, DockerManifestDetail, DockerTagInfo, Project, Repo } from '../api'
|
||||
import ProjectNavBar from '../components/ProjectNavBar'
|
||||
import RepoSubNav from '../components/RepoSubNav'
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
|
||||
|
||||
type RepoDockerDetailPageProps = {
|
||||
initialRepo?: Repo
|
||||
}
|
||||
|
||||
export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
|
||||
const { initialRepo } = props
|
||||
const { projectId, repoId } = useParams()
|
||||
const [repo, setRepo] = useState<Repo | null>(initialRepo || null)
|
||||
const [project, setProject] = useState<Project | null>(null)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [imagesError, setImagesError] = useState<string | null>(null)
|
||||
const [selectedImage, setSelectedImage] = useState('')
|
||||
const [tags, setTags] = useState<DockerTagInfo[]>([])
|
||||
const [tagsError, setTagsError] = useState<string | null>(null)
|
||||
const [selectedTag, setSelectedTag] = useState<DockerTagInfo | null>(null)
|
||||
const [detail, setDetail] = useState<DockerManifestDetail | null>(null)
|
||||
const [detailError, setDetailError] = useState<string | null>(null)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [deleteTagOpen, setDeleteTagOpen] = useState(false)
|
||||
const [deleteTagName, setDeleteTagName] = useState('')
|
||||
const [deleteTagConfirm, setDeleteTagConfirm] = useState('')
|
||||
const [deleteImageOpen, setDeleteImageOpen] = useState(false)
|
||||
const [deleteImagePath, setDeleteImagePath] = useState('')
|
||||
const [deleteImageLabel, setDeleteImageLabel] = useState('')
|
||||
const [deleteImageConfirm, setDeleteImageConfirm] = useState('')
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const initRepoRef = useRef<string | null>(null)
|
||||
const initProjectRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!repoId) return
|
||||
if (initRepoRef.current === repoId) return
|
||||
initRepoRef.current = repoId
|
||||
setLoadError(null)
|
||||
if (initialRepo && initialRepo.id === repoId) {
|
||||
setRepo(initialRepo)
|
||||
return
|
||||
}
|
||||
api.getRepo(repoId).then((data) => setRepo(data)).catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load repository'
|
||||
setLoadError(message)
|
||||
setRepo(null)
|
||||
})
|
||||
}, [repoId, initialRepo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return
|
||||
if (initProjectRef.current === projectId) return
|
||||
initProjectRef.current = projectId
|
||||
api
|
||||
.getProject(projectId)
|
||||
.then((data) => setProject(data))
|
||||
.catch(() => setProject(null))
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!repoId) return
|
||||
if (!repo || repo.type !== 'docker') {
|
||||
setImages([])
|
||||
setImagesError(null)
|
||||
setSelectedImage('')
|
||||
return
|
||||
}
|
||||
api
|
||||
.listDockerImages(repoId)
|
||||
.then((data) => {
|
||||
const list = Array.isArray(data) ? data : []
|
||||
setImages(list)
|
||||
setImagesError(null)
|
||||
if (list.length && !selectedImage) {
|
||||
setSelectedImage(list[0])
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load images'
|
||||
setImagesError(message)
|
||||
setImages([])
|
||||
})
|
||||
}, [repoId, repo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!repoId) return
|
||||
if (!repo || repo.type !== 'docker') {
|
||||
setTags([])
|
||||
setTagsError(null)
|
||||
return
|
||||
}
|
||||
api
|
||||
.listDockerTags(repoId, selectedImage)
|
||||
.then((data) => {
|
||||
setTags(Array.isArray(data) ? data : [])
|
||||
setTagsError(null)
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load tags'
|
||||
setTagsError(message)
|
||||
setTags([])
|
||||
})
|
||||
}, [repoId, repo, selectedImage])
|
||||
|
||||
const handleTagSelect = (tag: DockerTagInfo) => {
|
||||
if (!repoId) return
|
||||
setSelectedTag(tag)
|
||||
setDetail(null)
|
||||
setDetailError(null)
|
||||
setDetailLoading(true)
|
||||
api
|
||||
.getDockerManifest(repoId, tag.tag, selectedImage)
|
||||
.then((data) => {
|
||||
setDetail(data)
|
||||
setDetailError(null)
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load manifest'
|
||||
setDetailError(message)
|
||||
})
|
||||
.finally(() => {
|
||||
setDetailLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const refreshImages = () => {
|
||||
if (!repoId) return
|
||||
api
|
||||
.listDockerImages(repoId)
|
||||
.then((data) => {
|
||||
const list = Array.isArray(data) ? data : []
|
||||
setImages(list)
|
||||
setImagesError(null)
|
||||
if (list.length && !list.includes(selectedImage)) {
|
||||
setSelectedImage(list[0])
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load images'
|
||||
setImagesError(message)
|
||||
setImages([])
|
||||
})
|
||||
}
|
||||
|
||||
const refreshTags = () => {
|
||||
if (!repoId) return
|
||||
api
|
||||
.listDockerTags(repoId, selectedImage)
|
||||
.then((data) => {
|
||||
setTags(Array.isArray(data) ? data : [])
|
||||
setTagsError(null)
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load tags'
|
||||
setTagsError(message)
|
||||
setTags([])
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteTag = async () => {
|
||||
if (!repoId || !deleteTagName) return
|
||||
if (deleteTagConfirm.trim() !== deleteTagName) {
|
||||
setDeleteError('Type the tag name to confirm deletion.')
|
||||
return
|
||||
}
|
||||
setDeleteError(null)
|
||||
setDeleting(true)
|
||||
try {
|
||||
await api.deleteDockerTag(repoId, selectedImage, deleteTagName)
|
||||
setDeleteTagOpen(false)
|
||||
setDeleteTagConfirm('')
|
||||
setDeleteTagName('')
|
||||
refreshTags()
|
||||
setDetail(null)
|
||||
setSelectedTag(null)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to delete tag'
|
||||
setDeleteError(message)
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteImage = async () => {
|
||||
if (!repoId) return
|
||||
if (deleteImageConfirm.trim() !== deleteImageLabel) {
|
||||
setDeleteError('Type the image name to confirm deletion.')
|
||||
return
|
||||
}
|
||||
setDeleteError(null)
|
||||
setDeleting(true)
|
||||
try {
|
||||
await api.deleteDockerImage(repoId, deleteImagePath)
|
||||
setDeleteImageOpen(false)
|
||||
setDeleteImageConfirm('')
|
||||
setDeleteImagePath('')
|
||||
setDeleteImageLabel('')
|
||||
setSelectedImage('')
|
||||
setSelectedTag(null)
|
||||
setDetail(null)
|
||||
refreshImages()
|
||||
refreshTags()
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to delete image'
|
||||
setDeleteError(message)
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button component={Link} to="/projects" size="small">
|
||||
Projects
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
/
|
||||
</Typography>
|
||||
{project ? (
|
||||
<>
|
||||
<Button component={Link} to={`/projects/${projectId}`} size="small">
|
||||
{project.name}
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
/
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
<Button component={Link} to={`/projects/${projectId}/repos`} size="small">
|
||||
Repositories
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
/
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{repo?.name || 'Repository'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{projectId ? <ProjectNavBar projectId={projectId} sx={{ mb: 0, minWidth: 320 }} /> : null}
|
||||
</Box>
|
||||
{projectId && repoId ? <RepoSubNav projectId={projectId} repoId={repoId} repoType={repo?.type} /> : null}
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
{loadError ? (
|
||||
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
|
||||
{loadError}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '280px minmax(0, 1fr)', gap: 1, mb: 1 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
{imagesError ? (
|
||||
<Alert severity="warning" sx={{ mb: 1 }}>
|
||||
{imagesError}
|
||||
</Alert>
|
||||
) : null}
|
||||
<List dense>
|
||||
{images.map((image) => (
|
||||
<ListItem key={image || 'root'} disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setSelectedImage(image)
|
||||
setSelectedTag(null)
|
||||
setDetail(null)
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ fontWeight: selectedImage === image ? 600 : 400 }}>
|
||||
{image || '(root)'}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setDeleteError(null)
|
||||
setDeleteImagePath(image)
|
||||
setDeleteImageLabel(image || '(root)')
|
||||
setDeleteImageConfirm('')
|
||||
setDeleteImageOpen(true)
|
||||
}}
|
||||
aria-label={`Delete image ${image || '(root)'}`}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{!images.length && !imagesError ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}>
|
||||
No images found.
|
||||
</Typography>
|
||||
) : null}
|
||||
</List>
|
||||
</Paper>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
{tagsError ? (
|
||||
<Alert severity="warning" sx={{ mb: 1 }}>
|
||||
{tagsError}
|
||||
</Alert>
|
||||
) : null}
|
||||
{selectedImage !== '' || images.includes('') ? (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Image: {selectedImage || '(root)'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
<List dense>
|
||||
{tags.map((tag) => (
|
||||
<ListItem key={tag.tag} disablePadding>
|
||||
<ListItemButton onClick={() => handleTagSelect(tag)}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="body2" sx={{ fontWeight: selectedTag?.tag === tag.tag ? 600 : 400 }}>
|
||||
{tag.tag}
|
||||
</Typography>
|
||||
}
|
||||
secondary={tag.digest ? tag.digest.slice(0, 12) : ''}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setDeleteError(null)
|
||||
setDeleteTagName(tag.tag)
|
||||
setDeleteTagConfirm('')
|
||||
setDeleteTagOpen(true)
|
||||
}}
|
||||
aria-label={`Delete tag ${tag.tag}`}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{!tags.length && !tagsError ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ px: 1, py: 1 }}>
|
||||
No tags found.
|
||||
</Typography>
|
||||
) : null}
|
||||
</List>
|
||||
{detailLoading ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Loading manifest...
|
||||
</Typography>
|
||||
) : null}
|
||||
{detailError ? (
|
||||
<Alert severity="warning">{detailError}</Alert>
|
||||
) : null}
|
||||
{detail ? (
|
||||
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||
<Typography variant="body2">Tag: {detail.reference}</Typography>
|
||||
<Typography variant="body2">Digest: {detail.digest}</Typography>
|
||||
<Typography variant="body2">Media: {detail.media_type}</Typography>
|
||||
<Typography variant="body2">Size: {detail.size} bytes</Typography>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Typography variant="body2">OS: {detail.config?.os || 'n/a'}</Typography>
|
||||
<Typography variant="body2">Architecture: {detail.config?.architecture || 'n/a'}</Typography>
|
||||
<Typography variant="body2">
|
||||
Created: {detail.config?.created ? new Date(detail.config.created).toLocaleString() : 'n/a'}
|
||||
</Typography>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<Typography variant="body2">Layers: {detail.layers?.length || 0}</Typography>
|
||||
<List dense>
|
||||
{detail.layers?.map((layer) => (
|
||||
<ListItem key={layer.digest}>
|
||||
<ListItemText primary={layer.digest} secondary={`${layer.size} bytes`} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Select a tag to view details.
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
<Dialog open={deleteTagOpen} onClose={() => setDeleteTagOpen(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Delete tag</DialogTitle>
|
||||
<DialogContent>
|
||||
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null}
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Delete tag "{deleteTagName}"?
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Type tag to confirm"
|
||||
value={deleteTagConfirm}
|
||||
onChange={(event) => setDeleteTagConfirm(event.target.value)}
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteTagOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteTag}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={deleting || deleteTagConfirm.trim() !== deleteTagName}
|
||||
>
|
||||
{deleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Dialog open={deleteImageOpen} onClose={() => setDeleteImageOpen(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Delete image</DialogTitle>
|
||||
<DialogContent>
|
||||
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null}
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Delete image "{deleteImageLabel || '(root)'}" and all tags?
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Type image name to confirm"
|
||||
value={deleteImageConfirm}
|
||||
onChange={(event) => setDeleteImageConfirm(event.target.value)}
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteImageOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteImage}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={deleting || deleteImageConfirm.trim() !== deleteImageLabel}
|
||||
>
|
||||
{deleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -463,7 +463,7 @@ export default function ReposPage() {
|
||||
<Chip label={repo.type || 'git'} size="small" variant="outlined" />
|
||||
{repo.is_foreign ? <Chip label="foreign" size="small" color="warning" variant="outlined" /> : null}
|
||||
{repo.is_foreign && repo.owner_slug ? <Chip label={repo.owner_slug} size="small" variant="outlined" /> : null}
|
||||
{(repo.type === 'rpm' ? repo.rpm_url : repo.clone_url) ? (
|
||||
{(repo.type === 'rpm' ? repo.rpm_url : repo.type === 'docker' ? repo.docker_url : repo.clone_url) ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
@@ -476,7 +476,7 @@ export default function ReposPage() {
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{repo.type === 'rpm' ? repo.rpm_url : repo.clone_url}
|
||||
{repo.type === 'rpm' ? repo.rpm_url : repo.type === 'docker' ? repo.docker_url : repo.clone_url}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
@@ -530,6 +530,7 @@ export default function ReposPage() {
|
||||
>
|
||||
<MenuItem value="git">Git</MenuItem>
|
||||
<MenuItem value="rpm">RPM</MenuItem>
|
||||
<MenuItem value="docker">Docker</MenuItem>
|
||||
</TextField>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1 }}>
|
||||
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
|
||||
|
||||
Reference in New Issue
Block a user