Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a59eb48b74 | |||
| 3c57ccf0fd |
@@ -1,6 +1,9 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import { ButtonProps } from '@mui/material/Button'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import { IconButtonProps } from '@mui/material/IconButton'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type ListRowActionsProps = {
|
||||
@@ -8,6 +11,9 @@ type ListRowActionsProps = {
|
||||
}
|
||||
|
||||
type ListRowActionButtonProps = Omit<ButtonProps, 'size' | 'variant'>
|
||||
type ListRowIconActionProps = Omit<IconButtonProps, 'size'> & {
|
||||
title: string
|
||||
}
|
||||
|
||||
export function ListRowActions(props: ListRowActionsProps) {
|
||||
return (
|
||||
@@ -31,3 +37,15 @@ export function ListRowActions(props: ListRowActionsProps) {
|
||||
export function ListRowActionButton(props: ListRowActionButtonProps) {
|
||||
return <Button variant="outlined" size="small" {...props} />
|
||||
}
|
||||
|
||||
export function ListRowIconAction({ title, children, ...buttonProps }: ListRowIconActionProps) {
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<span>
|
||||
<IconButton size="small" aria-label={title} {...buttonProps}>
|
||||
{children}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -13,7 +11,7 @@ import { api, Board } from '../api'
|
||||
import BoardFormDialog from '../components/BoardFormDialog'
|
||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||
import HeaderActionButton from '../components/HeaderActionButton'
|
||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||
import { ListRowActions, ListRowIconAction } from '../components/ListRowActions'
|
||||
import PaginationControls from '../components/PaginationControls'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import ProjectPageFrame from '../components/ProjectPageFrame'
|
||||
@@ -122,7 +120,6 @@ export default function ProjectBoardsPage() {
|
||||
display: 'block',
|
||||
textDecoration: 'none',
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -147,35 +144,26 @@ export default function ProjectBoardsPage() {
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'Actions',
|
||||
defaultWidth: 300,
|
||||
minWidth: 300,
|
||||
defaultWidth: 96,
|
||||
minWidth: 96,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
cellSx: { textAlign: 'right', whiteSpace: 'nowrap' },
|
||||
renderCell: (board: Board) => (
|
||||
<ListRowActions>
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/boards/${board.id}`}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenInNewIcon />}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<ListRowActionButton
|
||||
startIcon={<EditIcon />}
|
||||
<ListRowIconAction
|
||||
title="Edit board"
|
||||
onClick={() => { setEditBoard(board); setEditError(null) }}
|
||||
>
|
||||
Edit
|
||||
</ListRowActionButton>
|
||||
<ListRowActionButton
|
||||
startIcon={<DeleteIcon />}
|
||||
<EditIcon fontSize="small" />
|
||||
</ListRowIconAction>
|
||||
<ListRowIconAction
|
||||
title="Delete board"
|
||||
color="error"
|
||||
onClick={() => { setDeleteBoard(board); setDeleteError(null); setDeleteConfirmValue('') }}
|
||||
>
|
||||
Delete
|
||||
</ListRowActionButton>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</ListRowIconAction>
|
||||
</ListRowActions>
|
||||
),
|
||||
},
|
||||
@@ -188,7 +176,7 @@ export default function ProjectBoardsPage() {
|
||||
title="Boards"
|
||||
actions={
|
||||
<HeaderActionButton variant="outlined" startIcon={<AddIcon />} onClick={() => setCreateOpen(true)}>
|
||||
New Board
|
||||
New
|
||||
</HeaderActionButton>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import LinkOffIcon from '@mui/icons-material/LinkOff'
|
||||
@@ -9,23 +10,31 @@ import {
|
||||
Box,
|
||||
Chip,
|
||||
Checkbox,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MouseEvent, useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import { api, AvailableRepo, Project, Repo, RepoStats } from '../api'
|
||||
import ProjectPageFrame from '../components/ProjectPageFrame'
|
||||
import HeaderActionButton from '../components/HeaderActionButton'
|
||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||
import { ListRowActions, ListRowIconAction } from '../components/ListRowActions'
|
||||
import PaginationControls from '../components/PaginationControls'
|
||||
import ResizableSortableTable, { RSTColumn } from '../components/ResizableSortableTable'
|
||||
import RepositoryFormDialog from '../components/RepositoryFormDialog'
|
||||
import SectionCard from '../components/SectionCard'
|
||||
import SelectField from '../components/SelectField'
|
||||
|
||||
type RepoAccessInfo = {
|
||||
label: string
|
||||
displayValue: string
|
||||
copyValue: string
|
||||
}
|
||||
|
||||
export default function ReposPage() {
|
||||
const { projectId } = useParams()
|
||||
const [repos, setRepos] = useState<Repo[]>([])
|
||||
@@ -337,10 +346,32 @@ export default function ReposPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function repoSource(repo: Repo): string {
|
||||
if (repo.type === 'rpm') return repo.rpm_url || ''
|
||||
if (repo.type === 'docker') return repo.docker_url || ''
|
||||
return repo.clone_url || ''
|
||||
function repoAccess(repo: Repo): RepoAccessInfo {
|
||||
let value: string
|
||||
if (repo.type === 'rpm') {
|
||||
value = repo.rpm_url || ''
|
||||
return { label: 'dnf/yum', displayValue: value, copyValue: fullAccessURL(value) }
|
||||
}
|
||||
if (repo.type === 'docker') {
|
||||
value = repo.docker_url || ''
|
||||
return { label: 'docker pull', displayValue: value, copyValue: fullAccessURL(value) }
|
||||
}
|
||||
value = repo.clone_url || ''
|
||||
return { label: 'git clone', displayValue: value, copyValue: fullAccessURL(value) }
|
||||
}
|
||||
|
||||
function fullAccessURL(value: string): string {
|
||||
const trimmed: string = value.trim()
|
||||
if (!trimmed) return ''
|
||||
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed
|
||||
if (trimmed.startsWith('//')) return `${window.location.protocol}${trimmed}`
|
||||
if (trimmed.startsWith('/')) return `${window.location.origin}${trimmed}`
|
||||
return `${window.location.origin}/${trimmed}`
|
||||
}
|
||||
|
||||
function copyRepoAccess(value: string) {
|
||||
if (!value || !navigator.clipboard || !navigator.clipboard.writeText) return
|
||||
void navigator.clipboard.writeText(value)
|
||||
}
|
||||
|
||||
const columns: RSTColumn<Repo>[] = [
|
||||
@@ -363,7 +394,7 @@ export default function ReposPage() {
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
defaultWidth: 200,
|
||||
defaultWidth: 150,
|
||||
minWidth: 100,
|
||||
getValue: (repo: Repo) => repo.name,
|
||||
renderCell: (repo: Repo) => (
|
||||
@@ -375,7 +406,6 @@ export default function ReposPage() {
|
||||
display: 'block',
|
||||
textDecoration: 'none',
|
||||
color: 'primary.main',
|
||||
fontWeight: 600,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -397,7 +427,7 @@ export default function ReposPage() {
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Description',
|
||||
defaultWidth: 300,
|
||||
defaultWidth: 250,
|
||||
minWidth: 160,
|
||||
getValue: (repo: Repo) => repo.description || '',
|
||||
renderCell: (repo: Repo) => (
|
||||
@@ -407,16 +437,40 @@ export default function ReposPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: 'Source',
|
||||
defaultWidth: 200,
|
||||
minWidth: 150,
|
||||
getValue: (repo: Repo) => repoSource(repo),
|
||||
renderCell: (repo: Repo) => (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{repoSource(repo) || '-'}
|
||||
key: 'access',
|
||||
label: 'Access',
|
||||
defaultWidth: 280,
|
||||
minWidth: 180,
|
||||
getValue: (repo: Repo) => {
|
||||
const access: RepoAccessInfo = repoAccess(repo)
|
||||
return `${access.label} ${access.displayValue}`
|
||||
},
|
||||
renderCell: (repo: Repo) => {
|
||||
const access: RepoAccessInfo = repoAccess(repo)
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
|
||||
<Chip label={access.label} size="small" variant="outlined" sx={{ flexShrink: 0 }} />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
|
||||
{access.displayValue || '-'}
|
||||
</Typography>
|
||||
),
|
||||
{access.copyValue ? (
|
||||
<Tooltip title="Copy access URL">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Copy access URL"
|
||||
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation()
|
||||
copyRepoAccess(access.copyValue)
|
||||
}}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
@@ -425,35 +479,38 @@ export default function ReposPage() {
|
||||
minWidth: 50,
|
||||
getValue: (repo: Repo) => `${repo.is_foreign ? 'foreign' : 'local'} ${repo.owner_slug || ''}`,
|
||||
renderCell: (repo: Repo) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{repo.is_foreign ? <Chip label="foreign" size="small" color="warning" variant="outlined" /> : <Chip label="local" size="small" variant="outlined" />}
|
||||
{repo.is_foreign && repo.owner_slug ? <Chip label={repo.owner_slug} size="small" variant="outlined" /> : null}
|
||||
</Box>
|
||||
repo.is_foreign ? (
|
||||
<Tooltip title={repo.owner_slug ? `Owned by ${repo.owner_slug}` : 'Foreign repository'}>
|
||||
<Chip label="foreign" size="small" color="warning" variant="outlined" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Chip label="local" size="small" variant="outlined" />
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'Actions',
|
||||
defaultWidth: 210,
|
||||
minWidth: 210,
|
||||
defaultWidth: 96,
|
||||
minWidth: 96,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
cellSx: { textAlign: 'right', whiteSpace: 'nowrap' },
|
||||
renderCell: (repo: Repo) => (
|
||||
!repo.is_foreign ? (
|
||||
<ListRowActions>
|
||||
<ListRowActionButton startIcon={<EditIcon />} onClick={() => openEdit(repo)}>
|
||||
Edit
|
||||
</ListRowActionButton>
|
||||
<ListRowActionButton startIcon={<DeleteIcon />} color="error" onClick={() => openDelete(repo)}>
|
||||
Delete
|
||||
</ListRowActionButton>
|
||||
<ListRowIconAction title="Edit repository" onClick={() => openEdit(repo)}>
|
||||
<EditIcon fontSize="small" />
|
||||
</ListRowIconAction>
|
||||
<ListRowIconAction title="Delete repository" color="error" onClick={() => openDelete(repo)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</ListRowIconAction>
|
||||
</ListRowActions>
|
||||
) : (
|
||||
<ListRowActions>
|
||||
<ListRowActionButton startIcon={<LinkOffIcon />} onClick={() => openDetachForeign(repo)}>
|
||||
Detach
|
||||
</ListRowActionButton>
|
||||
<ListRowIconAction title="Detach repository" onClick={() => openDetachForeign(repo)}>
|
||||
<LinkOffIcon fontSize="small" />
|
||||
</ListRowIconAction>
|
||||
</ListRowActions>
|
||||
)
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user