Merge "Add provider image web ui"
This commit is contained in:
commit
a5f94c23e2
@ -1424,6 +1424,7 @@ class TestWebProviders(LauncherBaseTestCase, WebMixin):
|
||||
dict(name='build-debian-local-image', result='SUCCESS'),
|
||||
dict(name='build-ubuntu-local-image', result='SUCCESS'),
|
||||
], ordered=False)
|
||||
|
||||
resp = self.get_url('api/tenant/tenant-one/images')
|
||||
data = resp.json()
|
||||
self.assertEqual(4, len(data))
|
||||
@ -1761,8 +1762,8 @@ class TestInfo(BaseTestWeb):
|
||||
statsd_config = self.config_ini_data.get('statsd', {})
|
||||
self.stats_prefix = statsd_config.get('prefix')
|
||||
|
||||
def _expected_info(self):
|
||||
return {
|
||||
def _expected_info(self, niz=None):
|
||||
ret = {
|
||||
"info": {
|
||||
"capabilities": {
|
||||
"job_history": True,
|
||||
@ -1780,6 +1781,9 @@ class TestInfo(BaseTestWeb):
|
||||
"websocket_url": self.websocket_url,
|
||||
}
|
||||
}
|
||||
if niz is not None:
|
||||
ret['info']['capabilities']['auth']['niz'] = niz
|
||||
return ret
|
||||
|
||||
def test_info(self):
|
||||
info = self.get_url("api/info").json()
|
||||
@ -1788,7 +1792,7 @@ class TestInfo(BaseTestWeb):
|
||||
|
||||
def test_tenant_info(self):
|
||||
info = self.get_url("api/tenant/tenant-one/info").json()
|
||||
expected_info = self._expected_info()
|
||||
expected_info = self._expected_info(niz=False)
|
||||
expected_info['info']['tenant'] = 'tenant-one'
|
||||
self.assertEqual(
|
||||
info, expected_info)
|
||||
@ -1798,7 +1802,7 @@ class TestWebCapabilitiesInfo(TestInfo):
|
||||
|
||||
config_file = 'zuul-admin-web-oidc.conf'
|
||||
|
||||
def _expected_info(self):
|
||||
def _expected_info(self, niz=None):
|
||||
info = super(TestWebCapabilitiesInfo, self)._expected_info()
|
||||
info['info']['capabilities']['auth'] = {
|
||||
'realms': {
|
||||
@ -1828,6 +1832,8 @@ class TestWebCapabilitiesInfo(TestInfo):
|
||||
'default_realm': 'myOIDC1',
|
||||
'read_protected': False,
|
||||
}
|
||||
if niz is not None:
|
||||
info['info']['capabilities']['auth']['niz'] = niz
|
||||
return info
|
||||
|
||||
|
||||
@ -1836,7 +1842,7 @@ class TestTenantAuthRealmInfo(TestWebCapabilitiesInfo):
|
||||
tenant_config_file = 'config/authorization/rules-templating/main.yaml'
|
||||
|
||||
def test_tenant_info(self):
|
||||
expected_info = self._expected_info()
|
||||
expected_info = self._expected_info(niz=False)
|
||||
info = self.get_url("api/tenant/tenant-zero/info").json()
|
||||
expected_info['info']['tenant'] = 'tenant-zero'
|
||||
expected_info['info']['capabilities']['auth']['default_realm'] =\
|
||||
@ -1873,7 +1879,7 @@ class TestRootAuth(TestWebCapabilitiesInfo):
|
||||
self.assertEqual(expected_info, info)
|
||||
|
||||
def test_tenant_info(self):
|
||||
expected_info = self._expected_info()
|
||||
expected_info = self._expected_info(niz=False)
|
||||
info = self.get_url("api/tenant/tenant-zero/info").json()
|
||||
expected_info['info']['tenant'] = 'tenant-zero'
|
||||
expected_info['info']['capabilities']['auth']['default_realm'] =\
|
||||
|
@ -93,13 +93,13 @@ class App extends React.Component {
|
||||
isTenantDropdownOpen: false,
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
renderMenu(menu) {
|
||||
const { tenant } = this.props
|
||||
if (tenant.name) {
|
||||
return (
|
||||
<Nav aria-label="Nav" variant="horizontal">
|
||||
<NavList>
|
||||
{this.menu.filter(item => item.title).map(item => (
|
||||
{menu.filter(item => item.title).map(item => (
|
||||
<NavItem itemId={item.to} key={item.to}>
|
||||
<NavLink
|
||||
to={tenant.linkPrefix + item.to}
|
||||
@ -127,7 +127,7 @@ class App extends React.Component {
|
||||
user.isFetching)
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
renderContent = (menu) => {
|
||||
const { tenant, auth, user } = this.props
|
||||
const allRoutes = []
|
||||
|
||||
@ -150,7 +150,7 @@ class App extends React.Component {
|
||||
if (auth.info.read_protected && user.scope.length<1) {
|
||||
return <AuthRequiredPage/>
|
||||
}
|
||||
this.menu
|
||||
menu
|
||||
// Do not include '/tenants' route in white-label setup
|
||||
.filter(item =>
|
||||
(tenant.whiteLabel && !item.globalRoute) || !tenant.whiteLabel)
|
||||
@ -235,11 +235,6 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.menu = routes()
|
||||
}
|
||||
|
||||
handleKebabDropdownToggle = (isKebabDropdownOpen) => {
|
||||
this.setState({
|
||||
isKebabDropdownOpen
|
||||
@ -392,7 +387,8 @@ class App extends React.Component {
|
||||
const { isKebabDropdownOpen } = this.state
|
||||
const { notifications, tenantStatus, tenant, info, auth } = this.props
|
||||
|
||||
const nav = this.renderMenu()
|
||||
const menu = routes(auth.info)
|
||||
const nav = this.renderMenu(menu)
|
||||
|
||||
const kebabDropdownItems = []
|
||||
if (!info.tenant) {
|
||||
@ -515,7 +511,7 @@ class App extends React.Component {
|
||||
{notifications.length > 0 && this.renderNotifications(notifications)}
|
||||
<Page className="zuul-page" header={pageHeader} tertiaryNav={nav}>
|
||||
<ErrorBoundary>
|
||||
{this.renderContent()}
|
||||
{this.renderContent(menu)}
|
||||
</ErrorBoundary>
|
||||
</Page>
|
||||
</React.Fragment>
|
||||
|
61
web/src/actions/providers.js
Normal file
61
web/src/actions/providers.js
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as API from '../api'
|
||||
|
||||
export const PROVIDERS_FETCH_REQUEST = 'PROVIDERS_FETCH_REQUEST'
|
||||
export const PROVIDERS_FETCH_SUCCESS = 'PROVIDERS_FETCH_SUCCESS'
|
||||
export const PROVIDERS_FETCH_FAIL = 'PROVIDERS_FETCH_FAIL'
|
||||
|
||||
export const requestProviders = () => ({
|
||||
type: PROVIDERS_FETCH_REQUEST
|
||||
})
|
||||
|
||||
export const receiveProviders = (tenant, json) => ({
|
||||
type: PROVIDERS_FETCH_SUCCESS,
|
||||
tenant: tenant,
|
||||
providers: json,
|
||||
receivedAt: Date.now()
|
||||
})
|
||||
|
||||
const failedProviders = error => ({
|
||||
type: PROVIDERS_FETCH_FAIL,
|
||||
error
|
||||
})
|
||||
|
||||
export const fetchProviders = (tenant) => dispatch => {
|
||||
dispatch(requestProviders())
|
||||
return API.fetchProviders(tenant.apiPrefix)
|
||||
.then(response => dispatch(receiveProviders(tenant.name, response.data)))
|
||||
.catch(error => dispatch(failedProviders(error)))
|
||||
}
|
||||
|
||||
const shouldFetchProviders = (tenant, state) => {
|
||||
const providers = state.providers.providers[tenant.name]
|
||||
if (!providers) {
|
||||
return true
|
||||
}
|
||||
if (providers.isFetching) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const fetchProvidersIfNeeded = (tenant) => (dispatch, getState) => {
|
||||
if (shouldFetchProviders(tenant, getState())) {
|
||||
return dispatch(fetchProviders(tenant))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
@ -212,6 +212,10 @@ function fetchProjects(apiPrefix) {
|
||||
return makeRequest(apiPrefix + 'projects')
|
||||
}
|
||||
|
||||
function fetchProviders(apiPrefix) {
|
||||
return makeRequest(apiPrefix + 'providers')
|
||||
}
|
||||
|
||||
function fetchJob(apiPrefix, jobName) {
|
||||
return makeRequest(apiPrefix + 'job/' + jobName)
|
||||
}
|
||||
@ -361,6 +365,7 @@ export {
|
||||
fetchPipelines,
|
||||
fetchProject,
|
||||
fetchProjects,
|
||||
fetchProviders,
|
||||
fetchSemaphores,
|
||||
fetchStatus,
|
||||
fetchTenantInfo,
|
||||
|
178
web/src/containers/provider/ImageBuildTable.jsx
Normal file
178
web/src/containers/provider/ImageBuildTable.jsx
Normal file
@ -0,0 +1,178 @@
|
||||
// Copyright 2020 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
expandable,
|
||||
} from '@patternfly/react-table'
|
||||
import { Link } from 'react-router-dom'
|
||||
import ImageUploadTable from './ImageUploadTable'
|
||||
|
||||
function ImageBuildTable(props) {
|
||||
const { buildArtifacts, fetching } = props
|
||||
const [collapsedRows, setCollapsedRows] = React.useState([])
|
||||
const setRowCollapsed = (idx, isCollapsing = true) =>
|
||||
setCollapsedRows(prevCollapsed => {
|
||||
const otherCollapsedRows = prevCollapsed.filter(r => r !== idx)
|
||||
return isCollapsing ?
|
||||
[...otherCollapsedRows, idx] : otherCollapsedRows
|
||||
})
|
||||
const isRowCollapsed = idx => collapsedRows.includes(idx)
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'UUID',
|
||||
dataLabel: 'UUID',
|
||||
cellFormatters: [expandable],
|
||||
},
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataLabel: 'Timestamp',
|
||||
},
|
||||
{
|
||||
title: 'Validated',
|
||||
dataLabel: 'Validated',
|
||||
},
|
||||
{
|
||||
title: 'Build',
|
||||
dataLabel: 'Build',
|
||||
},
|
||||
]
|
||||
|
||||
function createImageBuildRow(rows, build) {
|
||||
return {
|
||||
id: rows.length,
|
||||
isOpen: !isRowCollapsed(rows.length),
|
||||
cells: [
|
||||
{
|
||||
title: build.uuid
|
||||
},
|
||||
{
|
||||
title: build.timestamp
|
||||
},
|
||||
{
|
||||
title: build.validated.toString()
|
||||
},
|
||||
{
|
||||
// TODO: This may not be a valid link if it's outside this tenant;
|
||||
// consider hiding it in that case.
|
||||
title: <Link to={`${tenant.linkPrefix}/build/${build.build_uuid}`}>
|
||||
{build.build_uuid}
|
||||
</Link>
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
function createImageUploadRow(rows, parent, build) {
|
||||
return {
|
||||
id: rows.length,
|
||||
parent: parent.id,
|
||||
cells: [
|
||||
{
|
||||
title: <ImageUploadTable uploads={build.uploads}
|
||||
fetching={fetching}/>
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveBuildArtifacts = buildArtifacts && buildArtifacts.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveBuildArtifacts) {
|
||||
rows = []
|
||||
buildArtifacts.forEach(build => {
|
||||
let buildRow = createImageBuildRow(rows, build)
|
||||
rows.push(buildRow)
|
||||
rows.push(createImageUploadRow(rows, buildRow, build))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title headingLevel="h3">
|
||||
Image Build Artifacts
|
||||
</Title>
|
||||
<Table
|
||||
aria-label="Image Build Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
onCollapse={(_event, rowIndex, isOpen) => {
|
||||
setRowCollapsed(rowIndex, !isOpen)
|
||||
}}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any build artifacts but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveBuildArtifacts && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No build artifacts found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ImageBuildTable.propTypes = {
|
||||
buildArtifacts: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default ImageBuildTable
|
64
web/src/containers/provider/ImageDetail.jsx
Normal file
64
web/src/containers/provider/ImageDetail.jsx
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
function ProviderDetail(props) {
|
||||
const {image} = props
|
||||
return (
|
||||
<>
|
||||
<DescriptionList isHorizontal
|
||||
style={{'--pf-c-description-list--RowGap': '0rem'}}
|
||||
className='pf-u-m-xl'>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Name
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{image.name}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Canonical Name
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{image.canonical_name}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Type
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{image.type}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProviderDetail.propTypes = {
|
||||
image: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default ProviderDetail
|
120
web/src/containers/provider/ImageTable.jsx
Normal file
120
web/src/containers/provider/ImageTable.jsx
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright 2020 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
function ImageTable(props) {
|
||||
const { images, fetching, linkPrefix } = props
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
]
|
||||
|
||||
function createImageRow(image) {
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: (
|
||||
<Link to={`${linkPrefix}/${image.name}`}>{image.name}</Link>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveImages = images && images.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveImages) {
|
||||
rows = images.map((image) => createImageRow(image))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title headingLevel="h3">
|
||||
Images
|
||||
</Title>
|
||||
<Table
|
||||
aria-label="Image Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any images but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveImages && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No images found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ImageTable.propTypes = {
|
||||
images: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
linkPrefix: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ImageTable
|
145
web/src/containers/provider/ImageUploadTable.jsx
Normal file
145
web/src/containers/provider/ImageUploadTable.jsx
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2020 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
|
||||
function ImageUploadTable(props) {
|
||||
const { uploads, fetching } = props
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'UUID',
|
||||
dataLabel: 'UUID',
|
||||
},
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataLabel: 'Timestamp',
|
||||
},
|
||||
{
|
||||
title: 'Validated',
|
||||
dataLabel: 'Validated',
|
||||
},
|
||||
{
|
||||
title: 'Endpoint',
|
||||
dataLabel: 'Endpoint',
|
||||
},
|
||||
{
|
||||
title: 'External ID',
|
||||
dataLabel: 'External ID',
|
||||
},
|
||||
]
|
||||
|
||||
function createImageUploadRow(upload) {
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: upload.uuid
|
||||
},
|
||||
{
|
||||
title: upload.timestamp
|
||||
},
|
||||
{
|
||||
title: upload.validated.toString()
|
||||
},
|
||||
{
|
||||
title: upload.endpoint_name
|
||||
},
|
||||
{
|
||||
title: upload.external_id
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveUploads = uploads && uploads.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveUploads) {
|
||||
rows = uploads.map((upload) => createImageUploadRow(upload))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title headingLevel="h3">
|
||||
Image Uploads
|
||||
</Title>
|
||||
<Table
|
||||
aria-label="Image Upload Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any build artifacts but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveUploads && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No image uploads found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ImageUploadTable.propTypes = {
|
||||
uploads: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default ImageUploadTable
|
25
web/src/containers/provider/ProviderDetail.jsx
Normal file
25
web/src/containers/provider/ProviderDetail.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
function ProviderDetail() {
|
||||
// TODO: add general provider information
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderDetail
|
123
web/src/containers/provider/ProviderTable.jsx
Normal file
123
web/src/containers/provider/ProviderTable.jsx
Normal file
@ -0,0 +1,123 @@
|
||||
// Copyright 2020 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
function ProviderTable(props) {
|
||||
const { providers, fetching, tenant } = props
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
]
|
||||
|
||||
function createProviderRow(provider) {
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: (
|
||||
<Link to={`${tenant.linkPrefix}/provider/${provider.name}`}>{provider.name}</Link>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveProviders = providers && providers.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveProviders) {
|
||||
rows = providers.map((provider) => createProviderRow(provider))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Provider Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any providers but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveProviders && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No providers found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProviderTable.propTypes = {
|
||||
providers: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
tenant: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
user: state.user,
|
||||
}))(ProviderTable)
|
84
web/src/pages/Provider.jsx
Normal file
84
web/src/pages/Provider.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
Level,
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import PropTypes from 'prop-types'
|
||||
import { fetchProviders, fetchProvidersIfNeeded } from '../actions/providers'
|
||||
import ProviderDetail from '../containers/provider/ProviderDetail'
|
||||
import ImageTable from '../containers/provider/ImageTable'
|
||||
import { ReloadButton } from '../containers/Fetching'
|
||||
|
||||
function ProviderPage(props) {
|
||||
const providerName = props.match.params.providerName
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
const providers = useSelector((state) => state.providers.providers[tenant.name])
|
||||
const isFetching = useSelector((state) => state.status.isFetching)
|
||||
const darkMode = useSelector((state) => state.preferences.darkMode)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const provider = useMemo(() =>
|
||||
providers?providers.find((e) => e.name === providerName):null,
|
||||
[providers, providerName])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Provider'
|
||||
dispatch(fetchProvidersIfNeeded(tenant))
|
||||
}, [tenant, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<Level>
|
||||
<LevelItem>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<ReloadButton
|
||||
isReloading={isFetching}
|
||||
reloadCallback={() => {dispatch(fetchProviders(tenant))}}
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<Title headingLevel="h2">
|
||||
Provider {providerName}
|
||||
</Title>
|
||||
{provider &&
|
||||
<>
|
||||
<ProviderDetail provider={provider}/>
|
||||
{provider.images &&
|
||||
<ImageTable
|
||||
images={provider.images}
|
||||
fetching={false}
|
||||
linkPrefix={`${tenant.linkPrefix}/provider/${providerName}/image`}/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProviderPage.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(ProviderPage)
|
88
web/src/pages/ProviderImage.jsx
Normal file
88
web/src/pages/ProviderImage.jsx
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
Level,
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import PropTypes from 'prop-types'
|
||||
import { fetchProviders, fetchProvidersIfNeeded } from '../actions/providers'
|
||||
import ImageDetail from '../containers/provider/ImageDetail'
|
||||
import ImageBuildTable from '../containers/provider/ImageBuildTable'
|
||||
import { ReloadButton } from '../containers/Fetching'
|
||||
|
||||
function ProviderImagePage(props) {
|
||||
const providerName = props.match.params.providerName
|
||||
const imageName = props.match.params.imageName
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
const providers = useSelector((state) => state.providers.providers[tenant.name])
|
||||
const isFetching = useSelector((state) => state.status.isFetching)
|
||||
const darkMode = useSelector((state) => state.preferences.darkMode)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const provider = useMemo(() =>
|
||||
providers?providers.find((e) => e.name === providerName):null,
|
||||
[providers, providerName])
|
||||
const image = useMemo(() =>
|
||||
provider?provider.images.find((e) => e.name === imageName):null,
|
||||
[provider, imageName])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Provider Image'
|
||||
dispatch(fetchProvidersIfNeeded(tenant))
|
||||
}, [tenant, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<Level>
|
||||
<LevelItem>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<ReloadButton
|
||||
isReloading={isFetching}
|
||||
reloadCallback={() => {dispatch(fetchProviders(tenant))}}
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<Title headingLevel="h2">
|
||||
Image {imageName} in {providerName}
|
||||
</Title>
|
||||
{image &&
|
||||
<>
|
||||
<ImageDetail image={image}/>
|
||||
{image.build_artifacts &&
|
||||
<ImageBuildTable
|
||||
buildArtifacts={image.build_artifacts}
|
||||
fetching={false}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProviderImagePage.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(ProviderImagePage)
|
61
web/src/pages/Providers.jsx
Normal file
61
web/src/pages/Providers.jsx
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
Level,
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core'
|
||||
import { fetchProviders, fetchProvidersIfNeeded } from '../actions/providers'
|
||||
import ProviderTable from '../containers/provider/ProviderTable'
|
||||
import { ReloadButton } from '../containers/Fetching'
|
||||
|
||||
function ProvidersPage() {
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
const providers = useSelector((state) => state.providers.providers[tenant.name])
|
||||
const isFetching = useSelector((state) => state.status.isFetching)
|
||||
const darkMode = useSelector((state) => state.preferences.darkMode)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Providers'
|
||||
dispatch(fetchProvidersIfNeeded(tenant))
|
||||
}, [tenant, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<Level>
|
||||
<LevelItem>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<ReloadButton
|
||||
isReloading={isFetching}
|
||||
reloadCallback={() => {dispatch(fetchProviders(tenant))}}
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<ProviderTable
|
||||
providers={providers}
|
||||
fetching={isFetching} />
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(ProvidersPage)
|
@ -33,6 +33,7 @@ import project from './project'
|
||||
import pipelines from './pipelines'
|
||||
import projects from './projects'
|
||||
import preferences from './preferences'
|
||||
import providers from './providers'
|
||||
import semaphores from './semaphores'
|
||||
import status from './status'
|
||||
import statusExpansion from './statusExpansion'
|
||||
@ -62,6 +63,7 @@ const reducers = {
|
||||
pipelines,
|
||||
project,
|
||||
projects,
|
||||
providers,
|
||||
semaphores,
|
||||
status,
|
||||
statusExpansion,
|
||||
|
48
web/src/reducers/providers.js
Normal file
48
web/src/reducers/providers.js
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022, 2024 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import {
|
||||
PROVIDERS_FETCH_FAIL,
|
||||
PROVIDERS_FETCH_REQUEST,
|
||||
PROVIDERS_FETCH_SUCCESS
|
||||
} from '../actions/providers'
|
||||
|
||||
export default (state = {
|
||||
isFetching: false,
|
||||
providers: {},
|
||||
}, action) => {
|
||||
switch (action.type) {
|
||||
case PROVIDERS_FETCH_REQUEST:
|
||||
return {
|
||||
isFetching: true,
|
||||
providers: state.providers,
|
||||
}
|
||||
case PROVIDERS_FETCH_SUCCESS:
|
||||
return {
|
||||
isFetching: false,
|
||||
providers: {
|
||||
...state.providers,
|
||||
[action.tenant]: action.providers
|
||||
}
|
||||
}
|
||||
case PROVIDERS_FETCH_FAIL:
|
||||
return {
|
||||
isFetching: false,
|
||||
providers: state.providers,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@ import FreezeJobPage from './pages/FreezeJob'
|
||||
import ChangeStatusPage from './pages/ChangeStatus'
|
||||
import ProjectPage from './pages/Project'
|
||||
import ProjectsPage from './pages/Projects'
|
||||
import ProvidersPage from './pages/Providers'
|
||||
import ProviderPage from './pages/Provider'
|
||||
import ProviderImagePage from './pages/ProviderImage'
|
||||
import JobPage from './pages/Job'
|
||||
import JobsPage from './pages/Jobs'
|
||||
import LabelsPage from './pages/Labels'
|
||||
@ -40,133 +43,157 @@ import PipelineOverviewPage from './pages/PipelineOverview'
|
||||
// Object with a title are created in the menu.
|
||||
// Object with globalRoute are not tenant scoped.
|
||||
// Remember to update the api getHomepageUrl subDir list for route with params
|
||||
const routes = () => [
|
||||
{
|
||||
title: 'Status',
|
||||
to: '/status',
|
||||
component: PipelineOverviewPage,
|
||||
},
|
||||
{
|
||||
title: 'Projects',
|
||||
to: '/projects',
|
||||
component: ProjectsPage
|
||||
},
|
||||
{
|
||||
title: 'Jobs',
|
||||
to: '/jobs',
|
||||
component: JobsPage
|
||||
},
|
||||
{
|
||||
title: 'Labels',
|
||||
to: '/labels',
|
||||
component: LabelsPage
|
||||
},
|
||||
{
|
||||
title: 'Nodes',
|
||||
to: '/nodes',
|
||||
component: NodesPage
|
||||
},
|
||||
{
|
||||
title: 'Autoholds',
|
||||
to: '/autoholds',
|
||||
component: AutoholdsPage
|
||||
},
|
||||
{
|
||||
title: 'Semaphores',
|
||||
to: '/semaphores',
|
||||
component: SemaphoresPage
|
||||
},
|
||||
{
|
||||
title: 'Builds',
|
||||
to: '/builds',
|
||||
component: BuildsPage
|
||||
},
|
||||
{
|
||||
title: 'Buildsets',
|
||||
to: '/buildsets',
|
||||
component: BuildsetsPage
|
||||
},
|
||||
{
|
||||
to: '/freeze-job',
|
||||
component: FreezeJobPage
|
||||
},
|
||||
{
|
||||
to: '/status/change/:changeId',
|
||||
component: ChangeStatusPage
|
||||