mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
Merge pull request #13712 from ansible/feature_usage-collection
Allow soft deletion of HostMetrics and add usage collection utility
This commit is contained in:
@@ -44,6 +44,7 @@ import WorkflowApprovalTemplates from './models/WorkflowApprovalTemplates';
|
||||
import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
|
||||
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
||||
import WorkflowJobs from './models/WorkflowJobs';
|
||||
import HostMetrics from './models/HostMetrics';
|
||||
|
||||
const ActivityStreamAPI = new ActivityStream();
|
||||
const AdHocCommandsAPI = new AdHocCommands();
|
||||
@@ -91,6 +92,7 @@ const WorkflowApprovalTemplatesAPI = new WorkflowApprovalTemplates();
|
||||
const WorkflowJobTemplateNodesAPI = new WorkflowJobTemplateNodes();
|
||||
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
||||
const WorkflowJobsAPI = new WorkflowJobs();
|
||||
const HostMetricsAPI = new HostMetrics();
|
||||
|
||||
export {
|
||||
ActivityStreamAPI,
|
||||
@@ -139,4 +141,5 @@ export {
|
||||
WorkflowJobTemplateNodesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
WorkflowJobsAPI,
|
||||
HostMetricsAPI,
|
||||
};
|
||||
|
||||
10
awx/ui/src/api/models/HostMetrics.js
Normal file
10
awx/ui/src/api/models/HostMetrics.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class HostMetrics extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = 'api/v2/host_metrics/';
|
||||
}
|
||||
}
|
||||
|
||||
export default HostMetrics;
|
||||
@@ -18,6 +18,10 @@ class Settings extends Base {
|
||||
return this.http.get(`${this.baseUrl}all/`);
|
||||
}
|
||||
|
||||
readSystem() {
|
||||
return this.http.get(`${this.baseUrl}system/`);
|
||||
}
|
||||
|
||||
updateCategory(category, data) {
|
||||
return this.http.patch(`${this.baseUrl}${category}/`, data);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ function DataListToolbar({
|
||||
enableRelatedFuzzyFiltering,
|
||||
handleIsAnsibleFactsSelected,
|
||||
isFilterCleared,
|
||||
advancedSearchDisabled,
|
||||
}) {
|
||||
const showExpandCollapse = onCompact && onExpand;
|
||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||
@@ -86,6 +87,10 @@ function DataListToolbar({
|
||||
}),
|
||||
[setIsKebabModalOpen]
|
||||
);
|
||||
const columns = [...searchColumns];
|
||||
if (!advancedSearchDisabled) {
|
||||
columns.push({ name: t`Advanced`, key: 'advanced' });
|
||||
}
|
||||
return (
|
||||
<Toolbar
|
||||
id={`${qsConfig.namespace}-list-toolbar`}
|
||||
@@ -134,10 +139,7 @@ function DataListToolbar({
|
||||
<ToolbarItem>
|
||||
<Search
|
||||
qsConfig={qsConfig}
|
||||
columns={[
|
||||
...searchColumns,
|
||||
{ name: t`Advanced`, key: 'advanced' },
|
||||
]}
|
||||
columns={columns}
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
onSearch={onSearch}
|
||||
@@ -224,6 +226,7 @@ DataListToolbar.propTypes = {
|
||||
additionalControls: PropTypes.arrayOf(PropTypes.node),
|
||||
enableNegativeFiltering: PropTypes.bool,
|
||||
enableRelatedFuzzyFiltering: PropTypes.bool,
|
||||
advancedSearchDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
DataListToolbar.defaultProps = {
|
||||
@@ -243,6 +246,7 @@ DataListToolbar.defaultProps = {
|
||||
additionalControls: [],
|
||||
enableNegativeFiltering: true,
|
||||
enableRelatedFuzzyFiltering: true,
|
||||
advancedSearchDisabled: false,
|
||||
};
|
||||
|
||||
export default DataListToolbar;
|
||||
|
||||
@@ -8,6 +8,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { useSession } from './Session';
|
||||
import { SettingsAPI } from '../api';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const ConfigContext = React.createContext({});
|
||||
@@ -40,6 +41,11 @@ export const ConfigProvider = ({ children }) => {
|
||||
},
|
||||
},
|
||||
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
||||
let systemConfig = {};
|
||||
if (me?.is_superuser || me?.is_system_auditor) {
|
||||
const { data: systemConfigResults } = await SettingsAPI.readSystem();
|
||||
systemConfig = systemConfigResults;
|
||||
}
|
||||
|
||||
const [
|
||||
{
|
||||
@@ -62,10 +68,21 @@ export const ConfigProvider = ({ children }) => {
|
||||
role_level: 'execution_environment_admin_role',
|
||||
}),
|
||||
]);
|
||||
|
||||
return { ...data, me, adminOrgCount, notifAdminCount, execEnvAdminCount };
|
||||
return {
|
||||
...data,
|
||||
me,
|
||||
adminOrgCount,
|
||||
notifAdminCount,
|
||||
execEnvAdminCount,
|
||||
systemConfig,
|
||||
};
|
||||
}, []),
|
||||
{ adminOrgCount: 0, notifAdminCount: 0, execEnvAdminCount: 0 }
|
||||
{
|
||||
adminOrgCount: 0,
|
||||
notifAdminCount: 0,
|
||||
execEnvAdminCount: 0,
|
||||
systemConfig: {},
|
||||
}
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(configError);
|
||||
@@ -112,6 +129,7 @@ export const useUserProfile = () => {
|
||||
isOrgAdmin: config.adminOrgCount,
|
||||
isNotificationAdmin: config.notifAdminCount,
|
||||
isExecEnvAdmin: config.execEnvAdminCount,
|
||||
systemConfig: config.systemConfig,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import TopologyView from 'screens/TopologyView';
|
||||
import Users from 'screens/User';
|
||||
import WorkflowApprovals from 'screens/WorkflowApproval';
|
||||
import { Jobs } from 'screens/Job';
|
||||
import HostMetrics from 'screens/HostMetrics';
|
||||
|
||||
function getRouteConfig(userProfile = {}) {
|
||||
let routeConfig = [
|
||||
@@ -55,6 +56,11 @@ function getRouteConfig(userProfile = {}) {
|
||||
path: '/workflow_approvals',
|
||||
screen: WorkflowApprovals,
|
||||
},
|
||||
{
|
||||
title: <Trans>Host Metrics</Trans>,
|
||||
path: '/host_metrics',
|
||||
screen: HostMetrics,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -178,9 +184,15 @@ function getRouteConfig(userProfile = {}) {
|
||||
const deleteRouteGroup = (name) => {
|
||||
routeConfig = routeConfig.filter(({ groupId }) => !groupId.includes(name));
|
||||
};
|
||||
|
||||
if (
|
||||
userProfile?.systemConfig?.SUBSCRIPTION_USAGE_MODEL !==
|
||||
'unique_managed_hosts'
|
||||
) {
|
||||
deleteRoute('host_metrics');
|
||||
}
|
||||
if (userProfile?.isSuperUser || userProfile?.isSystemAuditor)
|
||||
return routeConfig;
|
||||
deleteRoute('host_metrics');
|
||||
deleteRouteGroup('settings');
|
||||
deleteRoute('management_jobs');
|
||||
if (userProfile?.isOrgAdmin) return routeConfig;
|
||||
|
||||
@@ -7,6 +7,7 @@ const userProfile = {
|
||||
isOrgAdmin: false,
|
||||
isNotificationAdmin: false,
|
||||
isExecEnvAdmin: false,
|
||||
systemConfig: { SUBSCRIPTION_USAGE_MODEL: 'unique_managed_hosts' },
|
||||
};
|
||||
|
||||
const filterPaths = (sidebar) => {
|
||||
@@ -29,6 +30,7 @@ describe('getRouteConfig', () => {
|
||||
'/schedules',
|
||||
'/activity_stream',
|
||||
'/workflow_approvals',
|
||||
'/host_metrics',
|
||||
'/templates',
|
||||
'/credentials',
|
||||
'/projects',
|
||||
@@ -58,6 +60,7 @@ describe('getRouteConfig', () => {
|
||||
'/schedules',
|
||||
'/activity_stream',
|
||||
'/workflow_approvals',
|
||||
'/host_metrics',
|
||||
'/templates',
|
||||
'/credentials',
|
||||
'/projects',
|
||||
|
||||
156
awx/ui/src/screens/HostMetrics/HostMetrics.js
Normal file
156
awx/ui/src/screens/HostMetrics/HostMetrics.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
|
||||
import { HostMetricsAPI } from 'api';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import PaginatedTable, {
|
||||
HeaderRow,
|
||||
HeaderCell,
|
||||
} from 'components/PaginatedTable';
|
||||
import DataListToolbar from 'components/DataListToolbar';
|
||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import useSelected from 'hooks/useSelected';
|
||||
import HostMetricsListItem from './HostMetricsListItem';
|
||||
import HostMetricsDeleteButton from './HostMetricsDeleteButton';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host_metrics', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'hostname',
|
||||
deleted: false,
|
||||
});
|
||||
|
||||
function HostMetrics() {
|
||||
const location = useLocation();
|
||||
|
||||
const [breadcrumbConfig] = useState({
|
||||
'/host_metrics': t`Host Metrics`,
|
||||
});
|
||||
const {
|
||||
result: { count, results },
|
||||
isLoading,
|
||||
error,
|
||||
request: readHostMetrics,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const list = await HostMetricsAPI.read(params);
|
||||
return {
|
||||
count: list.data.count,
|
||||
results: list.data.results,
|
||||
};
|
||||
}, [location]),
|
||||
{ results: [], count: 0 }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
readHostMetrics();
|
||||
}, [readHostMetrics]);
|
||||
|
||||
const { selected, isAllSelected, handleSelect, selectAll, clearSelected } =
|
||||
useSelected(results);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScreenHeader streamType="none" breadcrumbConfig={breadcrumbConfig} />
|
||||
<PageSection>
|
||||
<Card>
|
||||
<PaginatedTable
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={results}
|
||||
itemCount={count}
|
||||
pluralizedItemName={t`Host Metrics`}
|
||||
renderRow={(item, index) => (
|
||||
<HostMetricsListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selected.some(
|
||||
(row) => row.hostname === item.hostname
|
||||
)}
|
||||
onSelect={() => handleSelect(item)}
|
||||
rowIndex={index}
|
||||
/>
|
||||
)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: t`Hostname`,
|
||||
key: 'hostname__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
]}
|
||||
toolbarSearchableKeys={[]}
|
||||
toolbarRelatedSearchableKeys={[]}
|
||||
renderToolbar={(props) => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
advancedSearchDisabled
|
||||
fillWidth
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={selectAll}
|
||||
additionalControls={[
|
||||
<HostMetricsDeleteButton
|
||||
key="delete"
|
||||
onDelete={() =>
|
||||
Promise.all(
|
||||
selected.map((hostMetric) =>
|
||||
HostMetricsAPI.destroy(hostMetric.id)
|
||||
)
|
||||
).then(() => {
|
||||
readHostMetrics();
|
||||
clearSelected();
|
||||
})
|
||||
}
|
||||
itemsToDelete={selected}
|
||||
pluralizedItemName={t`Host Metrics`}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG}>
|
||||
<HeaderCell sortKey="hostname">{t`Hostname`}</HeaderCell>
|
||||
<HeaderCell
|
||||
sortKey="first_automation"
|
||||
tooltip={t`When was the host first automated`}
|
||||
>
|
||||
{t`First automated`}
|
||||
</HeaderCell>
|
||||
<HeaderCell
|
||||
sortKey="last_automation"
|
||||
tooltip={t`When was the host last automated`}
|
||||
>
|
||||
{t`Last automated`}
|
||||
</HeaderCell>
|
||||
<HeaderCell
|
||||
sortKey="automated_counter"
|
||||
tooltip={t`How many times was the host automated`}
|
||||
>
|
||||
{t`Automation`}
|
||||
</HeaderCell>
|
||||
<HeaderCell
|
||||
sortKey="used_in_inventories"
|
||||
tooltip={t`How many inventories is the host in, recomputed on a weekly schedule`}
|
||||
>
|
||||
{t`Inventories`}
|
||||
</HeaderCell>
|
||||
<HeaderCell
|
||||
sortKey="deleted_counter"
|
||||
tooltip={t`How many times was the host deleted`}
|
||||
>
|
||||
{t`Deleted`}
|
||||
</HeaderCell>
|
||||
</HeaderRow>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { HostMetrics as _HostMetrics };
|
||||
export default HostMetrics;
|
||||
69
awx/ui/src/screens/HostMetrics/HostMetrics.test.js
Normal file
69
awx/ui/src/screens/HostMetrics/HostMetrics.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { HostMetricsAPI } from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import HostMetrics from './HostMetrics';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
const mockHostMetrics = [
|
||||
{
|
||||
hostname: 'Host name',
|
||||
first_automation: 'now',
|
||||
last_automation: 'now',
|
||||
automated_counter: 1,
|
||||
used_in_inventories: 1,
|
||||
deleted_counter: 1,
|
||||
id: 1,
|
||||
url: '',
|
||||
},
|
||||
];
|
||||
|
||||
function waitForLoaded(wrapper) {
|
||||
return waitForElement(
|
||||
wrapper,
|
||||
'HostList',
|
||||
(el) => el.find('ContentLoading').length === 0
|
||||
);
|
||||
}
|
||||
|
||||
describe('<HostMetrics />', () => {
|
||||
beforeEach(() => {
|
||||
HostMetricsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: mockHostMetrics.length,
|
||||
results: mockHostMetrics,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', async () => {
|
||||
await act(async () => {
|
||||
mountWithContexts(
|
||||
<HostMetrics
|
||||
match={{ path: '/hosts', url: '/hosts' }}
|
||||
location={{ search: '', pathname: '/hosts' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('HostMetrics are retrieved from the api and the components finishes loading', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<HostMetrics />);
|
||||
});
|
||||
await waitForLoaded(wrapper);
|
||||
|
||||
expect(HostMetricsAPI.read).toHaveBeenCalled();
|
||||
expect(wrapper.find('HostMetricsListItem')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
205
awx/ui/src/screens/HostMetrics/HostMetricsDeleteButton.js
Normal file
205
awx/ui/src/screens/HostMetrics/HostMetricsDeleteButton.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useState } from 'react';
|
||||
import { func, node, string, arrayOf, shape } from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Alert, Badge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getRelatedResourceDeleteCounts } from 'util/getRelatedResourceDeleteDetails';
|
||||
import AlertModal from '../../components/AlertModal';
|
||||
|
||||
import ErrorDetail from '../../components/ErrorDetail';
|
||||
|
||||
const WarningMessage = styled(Alert)`
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
&& {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ItemToDelete = shape({
|
||||
hostname: string.isRequired,
|
||||
});
|
||||
|
||||
function HostMetricsDeleteButton({
|
||||
itemsToDelete,
|
||||
pluralizedItemName,
|
||||
onDelete,
|
||||
deleteDetailsRequests,
|
||||
warningMessage,
|
||||
deleteMessage,
|
||||
}) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [deleteDetails, setDeleteDetails] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [deleteMessageError, setDeleteMessageError] = useState();
|
||||
const handleDelete = () => {
|
||||
onDelete();
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
const toggleModal = async (isOpen) => {
|
||||
setIsLoading(true);
|
||||
setDeleteDetails(null);
|
||||
if (
|
||||
isOpen &&
|
||||
itemsToDelete.length === 1 &&
|
||||
deleteDetailsRequests?.length > 0
|
||||
) {
|
||||
const { results, error } = await getRelatedResourceDeleteCounts(
|
||||
deleteDetailsRequests
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setDeleteMessageError(error);
|
||||
} else {
|
||||
setDeleteDetails(results);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsModalOpen(isOpen);
|
||||
};
|
||||
|
||||
const renderTooltip = () => {
|
||||
if (itemsToDelete.length) {
|
||||
return t`Soft delete`;
|
||||
}
|
||||
return t`Select a row to delete`;
|
||||
};
|
||||
|
||||
const modalTitle = t`Soft delete ${pluralizedItemName}?`;
|
||||
|
||||
const isDisabled = itemsToDelete.length === 0;
|
||||
|
||||
const buildDeleteWarning = () => {
|
||||
const deleteMessages = [];
|
||||
if (warningMessage) {
|
||||
deleteMessages.push(warningMessage);
|
||||
}
|
||||
if (deleteMessage) {
|
||||
if (itemsToDelete.length > 1 || deleteDetails) {
|
||||
deleteMessages.push(deleteMessage);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{deleteMessages.map((message) => (
|
||||
<div aria-label={message} key={message}>
|
||||
{message}
|
||||
</div>
|
||||
))}
|
||||
{deleteDetails &&
|
||||
Object.entries(deleteDetails).map(([key, value]) => (
|
||||
<div key={key} aria-label={`${key}: ${value}`}>
|
||||
<Label>{key}</Label>
|
||||
<Badge>{value}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (deleteMessageError) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={deleteMessageError}
|
||||
title={t`Error!`}
|
||||
onClose={() => {
|
||||
toggleModal(false);
|
||||
setDeleteMessageError();
|
||||
}}
|
||||
>
|
||||
<ErrorDetail error={deleteMessageError} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
const shouldShowDeleteWarning =
|
||||
warningMessage ||
|
||||
(itemsToDelete.length === 1 && deleteDetails) ||
|
||||
(itemsToDelete.length > 1 && deleteMessage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={renderTooltip()} position="top">
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isLoading}
|
||||
ouiaId="delete-button"
|
||||
spinnerAriaValueText={isLoading ? 'Loading' : undefined}
|
||||
aria-label={t`Delete`}
|
||||
onClick={() => toggleModal(true)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{t`Delete`}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isModalOpen && (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={modalTitle}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => toggleModal(false)}
|
||||
actions={[
|
||||
<Button
|
||||
ouiaId="delete-modal-confirm"
|
||||
key="delete"
|
||||
variant="danger"
|
||||
aria-label={t`confirm delete`}
|
||||
isDisabled={Boolean(
|
||||
deleteDetails && itemsToDelete[0]?.type === 'credential_type'
|
||||
)}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t`Delete`}
|
||||
</Button>,
|
||||
<Button
|
||||
ouiaId="delete-cancel"
|
||||
key="cancel"
|
||||
variant="link"
|
||||
aria-label={t`cancel delete`}
|
||||
onClick={() => toggleModal(false)}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div>{t`This action will soft delete the following:`}</div>
|
||||
{itemsToDelete.map((item) => (
|
||||
<span
|
||||
key={item.hostname}
|
||||
id={`item-to-be-deleted-${item.hostname}`}
|
||||
>
|
||||
<strong>{item.hostname}</strong>
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
{shouldShowDeleteWarning && (
|
||||
<WarningMessage
|
||||
variant="warning"
|
||||
isInline
|
||||
title={buildDeleteWarning()}
|
||||
/>
|
||||
)}
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
HostMetricsDeleteButton.propTypes = {
|
||||
onDelete: func.isRequired,
|
||||
itemsToDelete: arrayOf(ItemToDelete).isRequired,
|
||||
pluralizedItemName: string,
|
||||
warningMessage: node,
|
||||
};
|
||||
|
||||
HostMetricsDeleteButton.defaultProps = {
|
||||
pluralizedItemName: 'Items',
|
||||
warningMessage: null,
|
||||
};
|
||||
|
||||
export default HostMetricsDeleteButton;
|
||||
36
awx/ui/src/screens/HostMetrics/HostMetricsListItem.js
Normal file
36
awx/ui/src/screens/HostMetrics/HostMetricsListItem.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import { Tr, Td } from '@patternfly/react-table';
|
||||
import { formatDateString } from 'util/dates';
|
||||
import { HostMetrics } from 'types';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func } from 'prop-types';
|
||||
|
||||
function HostMetricsListItem({ item, isSelected, onSelect, rowIndex }) {
|
||||
return (
|
||||
<Tr
|
||||
id={`host_metrics-row-${item.hostname}`}
|
||||
ouiaId={`host-metrics-row-${item.hostname}`}
|
||||
>
|
||||
<Td select={{ rowIndex, isSelected, onSelect }} dataLabel={t`Selected`} />
|
||||
<Td dataLabel={t`Hostname`}>{item.hostname}</Td>
|
||||
<Td dataLabel={t`First automation`}>
|
||||
{formatDateString(item.first_automation)}
|
||||
</Td>
|
||||
<Td dataLabel={t`Last automation`}>
|
||||
{formatDateString(item.last_automation)}
|
||||
</Td>
|
||||
<Td dataLabel={t`Automation`}>{item.automated_counter}</Td>
|
||||
<Td dataLabel={t`Inventories`}>{item.used_in_inventories || 0}</Td>
|
||||
<Td dataLabel={t`Deleted`}>{item.deleted_counter}</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
HostMetricsListItem.propTypes = {
|
||||
item: HostMetrics.isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
|
||||
export default HostMetricsListItem;
|
||||
1
awx/ui/src/screens/HostMetrics/index.js
Normal file
1
awx/ui/src/screens/HostMetrics/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HostMetrics';
|
||||
@@ -439,3 +439,12 @@ export const Toast = shape({
|
||||
hasTimeout: bool,
|
||||
message: string,
|
||||
});
|
||||
|
||||
export const HostMetrics = shape({
|
||||
hostname: string.isRequired,
|
||||
first_automation: string.isRequired,
|
||||
last_automation: string.isRequired,
|
||||
automated_counter: number.isRequired,
|
||||
used_in_inventories: number,
|
||||
deleted_counter: number,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user