Merge "Add provider image web ui"

This commit is contained in:
Zuul 2025-01-16 23:17:27 +00:00 committed by Gerrit Code Review
commit a5f94c23e2
17 changed files with 1186 additions and 150 deletions

View File

@ -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'] =\

View File

@ -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>

View 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()
}

View File

@ -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,

View 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

View 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

View 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

View 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

View 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

View 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)

View 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)

View 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)

View 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)

View File

@ -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,

View 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
}
}

View File

@ -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