Add labels and flavors to web
This adds an API endpoint for flavors, and updates the existing API endpoint for labels to include niz-style labels. If a client needs to differentiate, it can do so by detecting the presesnce of a canonical_name field. The web UI does not need to differentiate; both types of labels will appear in the existing labels tab. A new tab is added to show flavors in the tenant. The provider page is updated with sub-tabs for labels and flavors relevant to the selected provider. Change-Id: I40dd56d85c6f208929a7ce8339a9be2484df404d
This commit is contained in:
@@ -1630,6 +1630,57 @@ class TestWebProviders(LauncherBaseTestCase, WebMixin):
|
||||
dict(name='build-ubuntu-local-image', result='SUCCESS'),
|
||||
], ordered=False)
|
||||
|
||||
@simple_layout('layouts/nodepool.yaml', enable_nodepool=True)
|
||||
def test_web_flavors(self):
|
||||
self.waitUntilSettled()
|
||||
self.startWebServer()
|
||||
resp = self.get_url('api/tenant/tenant-one/flavors')
|
||||
data = resp.json()
|
||||
self.assertEqual(4, len(data))
|
||||
|
||||
cc = 'review.example.com%2Forg%2Fcommon-config'
|
||||
expected = [
|
||||
{'canonical_name': f'{cc}/normal',
|
||||
'description': None,
|
||||
'name': 'normal'},
|
||||
{'canonical_name': f'{cc}/large',
|
||||
'description': None,
|
||||
'name': 'large'},
|
||||
{'canonical_name': f'{cc}/dedicated',
|
||||
'description': None,
|
||||
'name': 'dedicated'},
|
||||
{'canonical_name': f'{cc}/invalid',
|
||||
'description': None,
|
||||
'name': 'invalid'},
|
||||
]
|
||||
self.assertEqual(expected, data)
|
||||
|
||||
@simple_layout('layouts/nodepool.yaml', enable_nodepool=True)
|
||||
def test_web_labels(self):
|
||||
self.waitUntilSettled()
|
||||
self.startWebServer()
|
||||
resp = self.get_url('api/tenant/tenant-one/labels')
|
||||
data = resp.json()
|
||||
self.assertEqual(5, len(data))
|
||||
|
||||
cc = 'review.example.com%2Forg%2Fcommon-config'
|
||||
expected = [
|
||||
{'name': 'label1'},
|
||||
{'canonical_name': f'{cc}/debian-normal',
|
||||
'description': None,
|
||||
'name': 'debian-normal'},
|
||||
{'canonical_name': f'{cc}/debian-large',
|
||||
'description': None,
|
||||
'name': 'debian-large'},
|
||||
{'canonical_name': f'{cc}/debian-dedicated',
|
||||
'description': None,
|
||||
'name': 'debian-dedicated'},
|
||||
{'canonical_name': f'{cc}/debian-invalid',
|
||||
'description': None,
|
||||
'name': 'debian-invalid'},
|
||||
]
|
||||
self.assertEqual(expected, data)
|
||||
|
||||
|
||||
class TestWebStatusDisplayBranch(BaseTestWeb):
|
||||
tenant_config_file = 'config/change-queues/main.yaml'
|
||||
|
||||
61
web/src/actions/flavors.js
Normal file
61
web/src/actions/flavors.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 FLAVORS_FETCH_REQUEST = 'FLAVORS_FETCH_REQUEST'
|
||||
export const FLAVORS_FETCH_SUCCESS = 'FLAVORS_FETCH_SUCCESS'
|
||||
export const FLAVORS_FETCH_FAIL = 'FLAVORS_FETCH_FAIL'
|
||||
|
||||
export const requestFlavors = () => ({
|
||||
type: FLAVORS_FETCH_REQUEST
|
||||
})
|
||||
|
||||
export const receiveFlavors = (tenant, json) => ({
|
||||
type: FLAVORS_FETCH_SUCCESS,
|
||||
tenant: tenant,
|
||||
flavors: json,
|
||||
receivedAt: Date.now()
|
||||
})
|
||||
|
||||
const failedFlavors = error => ({
|
||||
type: FLAVORS_FETCH_FAIL,
|
||||
error
|
||||
})
|
||||
|
||||
export const fetchFlavors = (tenant) => dispatch => {
|
||||
dispatch(requestFlavors())
|
||||
return API.fetchFlavors(tenant.apiPrefix)
|
||||
.then(response => dispatch(receiveFlavors(tenant.name, response.data)))
|
||||
.catch(error => dispatch(failedFlavors(error)))
|
||||
}
|
||||
|
||||
const shouldFetchFlavors = (tenant, state) => {
|
||||
const flavors = state.flavors.flavors[tenant.name]
|
||||
if (!flavors) {
|
||||
return true
|
||||
}
|
||||
if (flavors.isFetching) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const fetchFlavorsIfNeeded = (tenant) => (dispatch, getState) => {
|
||||
if (shouldFetchFlavors(tenant, getState())) {
|
||||
return dispatch(fetchFlavors(tenant))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -225,6 +225,10 @@ function deleteImageUpload(apiPrefix, uploadId) {
|
||||
)
|
||||
}
|
||||
|
||||
function fetchFlavors(apiPrefix) {
|
||||
return makeRequest(apiPrefix + 'flavors')
|
||||
}
|
||||
|
||||
function fetchPipelines(apiPrefix) {
|
||||
return makeRequest(apiPrefix + 'pipelines')
|
||||
}
|
||||
@@ -382,6 +386,7 @@ export {
|
||||
fetchChangeStatus,
|
||||
fetchComponents,
|
||||
fetchConfigErrors,
|
||||
fetchFlavors,
|
||||
fetchFreezeJob,
|
||||
fetchImages,
|
||||
fetchInfo,
|
||||
|
||||
113
web/src/containers/provider/FlavorTable.jsx
Normal file
113
web/src/containers/provider/FlavorTable.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 FlavorTable(props) {
|
||||
const { flavors, fetching } = props
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
]
|
||||
|
||||
function createFlavorRow(flavor) {
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: flavor.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveFlavors = flavors && flavors.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveFlavors) {
|
||||
rows = flavors.map((flavor) => createFlavorRow(flavor))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Flavor 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 flavors but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveFlavors && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No flavors found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FlavorTable.propTypes = {
|
||||
flavors: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default FlavorTable
|
||||
@@ -83,9 +83,6 @@ function ImageTable(props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title headingLevel="h3">
|
||||
Images
|
||||
</Title>
|
||||
<Table
|
||||
aria-label="Image Table"
|
||||
variant={TableVariant.compact}
|
||||
|
||||
113
web/src/containers/provider/LabelTable.jsx
Normal file
113
web/src/containers/provider/LabelTable.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 LabelTable(props) {
|
||||
const { labels, fetching } = props
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
]
|
||||
|
||||
function createLabelRow(label) {
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: label.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveLabels = labels && labels.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveLabels) {
|
||||
rows = labels.map((label) => createLabelRow(label))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Label 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 labels but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveLabels && (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h1">No labels found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
LabelTable.propTypes = {
|
||||
labels: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default LabelTable
|
||||
69
web/src/pages/Flavors.jsx
Normal file
69
web/src/pages/Flavors.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { fetchFlavors, fetchFlavorsIfNeeded } from '../actions/flavors'
|
||||
import FlavorTable from '../containers/provider/FlavorTable'
|
||||
import { ReloadButton } from '../containers/Fetching'
|
||||
|
||||
function FlavorsPage() {
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
const flavors = useSelector((state) => state.flavors.flavors[tenant.name])
|
||||
const isFetching = useSelector((state) => state.status.isFetching)
|
||||
const darkMode = useSelector((state) => state.preferences.darkMode)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Flavors'
|
||||
dispatch(fetchFlavorsIfNeeded(tenant))
|
||||
}, [tenant, dispatch])
|
||||
|
||||
console.log(flavors)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<Level>
|
||||
<LevelItem>
|
||||
</LevelItem>
|
||||
<LevelItem>
|
||||
<ReloadButton
|
||||
isReloading={isFetching}
|
||||
reloadCallback={() => {dispatch(fetchFlavors(tenant))}}
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<Title headingLevel="h2">
|
||||
Flavors
|
||||
</Title>
|
||||
<FlavorTable
|
||||
flavors={flavors}
|
||||
fetching={isFetching}
|
||||
linkPrefix={`${tenant.linkPrefix}/flavor`}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(FlavorsPage)
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { fetchImages, fetchImagesIfNeeded } from '../actions/images'
|
||||
import ImageTable from '../containers/provider/ImageTable'
|
||||
@@ -52,6 +53,9 @@ function ImagesPage() {
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<Title headingLevel="h2">
|
||||
Images
|
||||
</Title>
|
||||
<ImageTable
|
||||
images={images}
|
||||
fetching={isFetching}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
@@ -20,12 +20,17 @@ import {
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabTitleText,
|
||||
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 FlavorTable from '../containers/provider/FlavorTable'
|
||||
import LabelTable from '../containers/provider/LabelTable'
|
||||
import { ReloadButton } from '../containers/Fetching'
|
||||
|
||||
function ProviderPage(props) {
|
||||
@@ -35,6 +40,7 @@ function ProviderPage(props) {
|
||||
const isFetching = useSelector((state) => state.status.isFetching)
|
||||
const darkMode = useSelector((state) => state.preferences.darkMode)
|
||||
const dispatch = useDispatch()
|
||||
const [activeTabKey, setActiveTabKey] = useState('images')
|
||||
|
||||
const provider = useMemo(() =>
|
||||
providers?providers.find((e) => e.name === providerName):null,
|
||||
@@ -45,6 +51,10 @@ function ProviderPage(props) {
|
||||
dispatch(fetchProvidersIfNeeded(tenant))
|
||||
}, [tenant, dispatch])
|
||||
|
||||
const handleTabClick = (event, tabIndex) => {
|
||||
setActiveTabKey(tabIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
@@ -64,12 +74,39 @@ function ProviderPage(props) {
|
||||
{provider &&
|
||||
<>
|
||||
<ProviderDetail provider={provider}/>
|
||||
{provider.images &&
|
||||
<ImageTable
|
||||
images={provider.images}
|
||||
fetching={false}
|
||||
linkPrefix={`${tenant.linkPrefix}/provider/${providerName}/image`}/>
|
||||
}
|
||||
<Tabs activeKey={activeTabKey} onSelect={handleTabClick}>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={<TabTitleText>Images</TabTitleText>}
|
||||
>
|
||||
{provider.images &&
|
||||
<ImageTable
|
||||
images={provider.images}
|
||||
fetching={false}
|
||||
linkPrefix={`${tenant.linkPrefix}/provider/${providerName}/image`}/>
|
||||
}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="flavors"
|
||||
title={<TabTitleText>Flavors</TabTitleText>}
|
||||
>
|
||||
{provider.flavors &&
|
||||
<FlavorTable
|
||||
flavors={provider.flavors}
|
||||
fetching={false}/>
|
||||
}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="labels"
|
||||
title={<TabTitleText>Labels</TabTitleText>}
|
||||
>
|
||||
{provider.labels &&
|
||||
<LabelTable
|
||||
labels={provider.labels}
|
||||
fetching={false}/>
|
||||
}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
}
|
||||
</PageSection>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
LevelItem,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { fetchProviders, fetchProvidersIfNeeded } from '../actions/providers'
|
||||
import ProviderTable from '../containers/provider/ProviderTable'
|
||||
@@ -50,6 +51,9 @@ function ProvidersPage() {
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
<Title headingLevel="h2">
|
||||
Providers
|
||||
</Title>
|
||||
<ProviderTable
|
||||
providers={providers}
|
||||
fetching={isFetching} />
|
||||
|
||||
48
web/src/reducers/flavors.js
Normal file
48
web/src/reducers/flavors.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 {
|
||||
FLAVORS_FETCH_FAIL,
|
||||
FLAVORS_FETCH_REQUEST,
|
||||
FLAVORS_FETCH_SUCCESS
|
||||
} from '../actions/flavors'
|
||||
|
||||
export default (state = {
|
||||
isFetching: false,
|
||||
flavors: {},
|
||||
}, action) => {
|
||||
switch (action.type) {
|
||||
case FLAVORS_FETCH_REQUEST:
|
||||
return {
|
||||
isFetching: true,
|
||||
flavors: state.flavors,
|
||||
}
|
||||
case FLAVORS_FETCH_SUCCESS:
|
||||
return {
|
||||
isFetching: false,
|
||||
flavors: {
|
||||
...state.flavors,
|
||||
[action.tenant]: action.flavors
|
||||
}
|
||||
}
|
||||
case FLAVORS_FETCH_FAIL:
|
||||
return {
|
||||
isFetching: false,
|
||||
flavors: state.flavors,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import auth from './auth'
|
||||
import autoholds from './autoholds'
|
||||
import change from './change'
|
||||
import component from './component'
|
||||
import flavors from './flavors'
|
||||
import freezejob from './freezejob'
|
||||
import notifications from './notifications'
|
||||
import build from './build'
|
||||
@@ -51,6 +52,7 @@ const reducers = {
|
||||
change,
|
||||
component,
|
||||
tenantStatus,
|
||||
flavors,
|
||||
freezejob,
|
||||
notifications,
|
||||
images,
|
||||
|
||||
@@ -26,6 +26,7 @@ import JobPage from './pages/Job'
|
||||
import JobsPage from './pages/Jobs'
|
||||
import ImagePage from './pages/Image'
|
||||
import ImagesPage from './pages/Images'
|
||||
import FlavorsPage from './pages/Flavors'
|
||||
import LabelsPage from './pages/Labels'
|
||||
import NodesPage from './pages/Nodes'
|
||||
import OpenApiPage from './pages/OpenApi'
|
||||
@@ -207,6 +208,13 @@ const routes = (info) => {
|
||||
component: ImagePage,
|
||||
}
|
||||
)
|
||||
ret.push(
|
||||
{
|
||||
title: 'Flavors',
|
||||
to: '/flavors',
|
||||
component: FlavorsPage,
|
||||
}
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -551,6 +551,48 @@ class ImageConverter:
|
||||
})
|
||||
|
||||
|
||||
class FlavorConverter:
|
||||
# A class to encapsulate the conversion of Flavor objects to
|
||||
# API output.
|
||||
@staticmethod
|
||||
def toDict(tenant, flavor):
|
||||
ret = {
|
||||
'name': flavor.name,
|
||||
'canonical_name': flavor.canonical_name,
|
||||
'description': flavor.description,
|
||||
}
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def schema():
|
||||
return Prop('The image', {
|
||||
'name': str,
|
||||
'canonical_name': str,
|
||||
'description': str,
|
||||
})
|
||||
|
||||
|
||||
class LabelConverter:
|
||||
# A class to encapsulate the conversion of Label objects to
|
||||
# API output.
|
||||
@staticmethod
|
||||
def toDict(tenant, label):
|
||||
ret = {
|
||||
'name': label.name,
|
||||
'canonical_name': label.canonical_name,
|
||||
'description': label.description,
|
||||
}
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def schema():
|
||||
return Prop('The image', {
|
||||
'name': str,
|
||||
'canonical_name': str,
|
||||
'description': str,
|
||||
})
|
||||
|
||||
|
||||
class APIError(cherrypy.HTTPError):
|
||||
def __init__(self, code, json_doc=None, headers=None):
|
||||
self._headers = headers or {}
|
||||
@@ -2029,6 +2071,24 @@ class ZuulWebAPI(object):
|
||||
upload.state = model.STATE_DELETING
|
||||
cherrypy.response.status = 204
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.save_params()
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
@cherrypy.tools.handle_options()
|
||||
@cherrypy.tools.check_tenant_auth()
|
||||
@openapi_response(
|
||||
code=200,
|
||||
content_type='application/json',
|
||||
description='Returns the list of flavors',
|
||||
schema=Prop('The list of flavors', [FlavorConverter.schema()]),
|
||||
)
|
||||
@openapi_response(404, 'Tenant not found')
|
||||
def flavors(self, tenant_name, tenant, auth):
|
||||
ret = []
|
||||
for flavor in tenant.layout.flavors.values():
|
||||
ret.append(FlavorConverter.toDict(tenant, flavor))
|
||||
return ret
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.save_params()
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
@@ -2043,6 +2103,8 @@ class ZuulWebAPI(object):
|
||||
launcher.supported_labels,
|
||||
allowed_labels, disallowed_labels))
|
||||
ret = [{'name': label} for label in sorted(labels)]
|
||||
for label in tenant.layout.labels.values():
|
||||
ret.append(LabelConverter.toDict(tenant, label))
|
||||
return ret
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -2745,6 +2807,8 @@ class ZuulWeb(object):
|
||||
controller=api, action='pipelines')
|
||||
route_map.connect('api', '/api/tenant/{tenant_name}/images',
|
||||
controller=api, action='images')
|
||||
route_map.connect('api', '/api/tenant/{tenant_name}/flavors',
|
||||
controller=api, action='flavors')
|
||||
route_map.connect('api',
|
||||
'/api/tenant/{tenant_name}/'
|
||||
'image/{image_name}/build',
|
||||
|
||||
Reference in New Issue
Block a user