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