-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add list Execution Environments (#8148)
See: #7886
- Loading branch information
Showing
7 changed files
with
461 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Base from '../Base'; | ||
|
||
class ExecutionEnvironments extends Base { | ||
constructor(http) { | ||
super(http); | ||
this.baseUrl = '/api/v2/execution_environments/'; | ||
} | ||
} | ||
|
||
export default ExecutionEnvironments; |
103 changes: 103 additions & 0 deletions
103
...rc/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import React from 'react'; | ||
import { act } from 'react-dom/test-utils'; | ||
|
||
import { | ||
mountWithContexts, | ||
waitForElement, | ||
} from '../../../../testUtils/enzymeHelpers'; | ||
|
||
import { ExecutionEnvironmentsAPI } from '../../../api'; | ||
import ExecutionEnvironmentList from './ExecutionEnvironmentList'; | ||
|
||
jest.mock('../../../api/models/ExecutionEnvironments'); | ||
|
||
const executionEnvironments = { | ||
data: { | ||
results: [ | ||
{ | ||
id: 1, | ||
image: 'https://registry.com/r/image/manifest', | ||
organization: null, | ||
credential: null, | ||
}, | ||
{ | ||
id: 2, | ||
image: 'https://registry.com/r/image2/manifest', | ||
organization: null, | ||
credential: null, | ||
}, | ||
], | ||
count: 2, | ||
}, | ||
}; | ||
|
||
const options = { data: { actions: { POST: true } } }; | ||
|
||
describe('<ExecutionEnvironmentList/>', () => { | ||
let wrapper; | ||
|
||
test('should mount successfully', async () => { | ||
await act(async () => { | ||
wrapper = mountWithContexts(<ExecutionEnvironmentList />); | ||
}); | ||
await waitForElement( | ||
wrapper, | ||
'ExecutionEnvironmentList', | ||
el => el.length > 0 | ||
); | ||
}); | ||
|
||
test('should have data fetched and render 2 rows', async () => { | ||
ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); | ||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); | ||
|
||
await act(async () => { | ||
wrapper = mountWithContexts(<ExecutionEnvironmentList />); | ||
}); | ||
await waitForElement( | ||
wrapper, | ||
'ExecutionEnvironmentList', | ||
el => el.length > 0 | ||
); | ||
|
||
expect(wrapper.find('ExecutionEnvironmentListItem').length).toBe(2); | ||
expect(ExecutionEnvironmentsAPI.read).toBeCalled(); | ||
expect(ExecutionEnvironmentsAPI.readOptions).toBeCalled(); | ||
}); | ||
|
||
test('should thrown content error', async () => { | ||
ExecutionEnvironmentsAPI.read.mockRejectedValue( | ||
new Error({ | ||
response: { | ||
config: { | ||
method: 'GET', | ||
url: '/api/v2/execution_environments', | ||
}, | ||
data: 'An error occurred', | ||
}, | ||
}) | ||
); | ||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options); | ||
await act(async () => { | ||
wrapper = mountWithContexts(<ExecutionEnvironmentList />); | ||
}); | ||
await waitForElement( | ||
wrapper, | ||
'ExecutionEnvironmentList', | ||
el => el.length > 0 | ||
); | ||
expect(wrapper.find('ContentError').length).toBe(1); | ||
}); | ||
|
||
test('should not render add button', async () => { | ||
ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); | ||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({ | ||
data: { actions: { POST: false } }, | ||
}); | ||
await act(async () => { | ||
wrapper = mountWithContexts(<ExecutionEnvironmentList />); | ||
}); | ||
waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); | ||
expect(wrapper.find('ToolbarAddButton').length).toBe(0); | ||
}); | ||
}); |
206 changes: 198 additions & 8 deletions
206
...xt/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,204 @@ | ||
import React from 'react'; | ||
import React, { useEffect, useCallback } from 'react'; | ||
import { useLocation, useRouteMatch } from 'react-router-dom'; | ||
import { withI18n } from '@lingui/react'; | ||
import { t } from '@lingui/macro'; | ||
import { Card, PageSection } from '@patternfly/react-core'; | ||
|
||
function ExecutionEnvironmentList() { | ||
import { ExecutionEnvironmentsAPI } from '../../../api'; | ||
import { getQSConfig, parseQueryString } from '../../../util/qs'; | ||
import useRequest, { useDeleteItems } from '../../../util/useRequest'; | ||
import useSelected from '../../../util/useSelected'; | ||
import PaginatedDataList, { | ||
ToolbarDeleteButton, | ||
ToolbarAddButton, | ||
} from '../../../components/PaginatedDataList'; | ||
import ErrorDetail from '../../../components/ErrorDetail'; | ||
import AlertModal from '../../../components/AlertModal'; | ||
import DatalistToolbar from '../../../components/DataListToolbar'; | ||
|
||
import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; | ||
|
||
const QS_CONFIG = getQSConfig('execution_environments', { | ||
page: 1, | ||
page_size: 20, | ||
managed_by_tower: false, | ||
order_by: 'image', | ||
}); | ||
|
||
function ExecutionEnvironmentList({ i18n }) { | ||
const location = useLocation(); | ||
const match = useRouteMatch(); | ||
|
||
const { | ||
error: contentError, | ||
isLoading, | ||
request: fetchExecutionEnvironments, | ||
result: { | ||
executionEnvironments, | ||
executionEnvironmentsCount, | ||
actions, | ||
relatedSearchableKeys, | ||
searchableKeys, | ||
}, | ||
} = useRequest( | ||
useCallback(async () => { | ||
const params = parseQueryString(QS_CONFIG, location.search); | ||
|
||
const [response, responseActions] = await Promise.all([ | ||
ExecutionEnvironmentsAPI.read(params), | ||
ExecutionEnvironmentsAPI.readOptions(), | ||
]); | ||
|
||
return { | ||
executionEnvironments: response.data.results, | ||
executionEnvironmentsCount: response.data.count, | ||
actions: responseActions.data.actions, | ||
relatedSearchableKeys: ( | ||
responseActions?.data?.related_search_fields || [] | ||
).map(val => val.slice(0, -8)), | ||
searchableKeys: Object.keys( | ||
responseActions.data.actions?.GET || {} | ||
).filter(key => responseActions.data.actions?.GET[key].filterable), | ||
}; | ||
}, [location]), | ||
{ | ||
executionEnvironments: [], | ||
executionEnvironmentsCount: 0, | ||
actions: {}, | ||
relatedSearchableKeys: [], | ||
searchableKeys: [], | ||
} | ||
); | ||
|
||
useEffect(() => { | ||
fetchExecutionEnvironments(); | ||
}, [fetchExecutionEnvironments]); | ||
|
||
const { selected, isAllSelected, handleSelect, setSelected } = useSelected( | ||
executionEnvironments | ||
); | ||
|
||
const { | ||
isLoading: deleteLoading, | ||
deletionError, | ||
deleteItems: deleteExecutionEnvironments, | ||
clearDeletionError, | ||
} = useDeleteItems( | ||
useCallback(async () => { | ||
await Promise.all( | ||
selected.map(({ id }) => ExecutionEnvironmentsAPI.destroy(id)) | ||
); | ||
}, [selected]), | ||
{ | ||
qsConfig: QS_CONFIG, | ||
allItemsSelected: isAllSelected, | ||
fetchItems: fetchExecutionEnvironments, | ||
} | ||
); | ||
|
||
const handleDelete = async () => { | ||
await deleteExecutionEnvironments(); | ||
setSelected([]); | ||
}; | ||
|
||
const canAdd = actions && actions.POST; | ||
|
||
return ( | ||
<PageSection> | ||
<Card> | ||
<div>List Execution environments</div> | ||
</Card> | ||
</PageSection> | ||
<> | ||
<PageSection> | ||
<Card> | ||
<PaginatedDataList | ||
contentError={contentError} | ||
hasContentLoading={isLoading || deleteLoading} | ||
items={executionEnvironments} | ||
itemCount={executionEnvironmentsCount} | ||
pluralizedItemName={i18n._(t`Execution Environments`)} | ||
qsConfig={QS_CONFIG} | ||
onRowClick={handleSelect} | ||
toolbarSearchableKeys={searchableKeys} | ||
toolbarRelatedSearchableKeys={relatedSearchableKeys} | ||
toolbarSearchColumns={[ | ||
{ | ||
name: i18n._(t`Image`), | ||
key: 'image__icontains', | ||
isDefault: true, | ||
}, | ||
]} | ||
toolbarSortColumns={[ | ||
{ | ||
name: i18n._(t`Image`), | ||
key: 'image', | ||
}, | ||
{ | ||
name: i18n._(t`Created`), | ||
key: 'created', | ||
}, | ||
{ | ||
name: i18n._(t`Organization`), | ||
key: 'organization', | ||
}, | ||
{ | ||
name: i18n._(t`Description`), | ||
key: 'description', | ||
}, | ||
]} | ||
renderToolbar={props => ( | ||
<DatalistToolbar | ||
{...props} | ||
showSelectAll | ||
isAllSelected={isAllSelected} | ||
onSelectAll={isSelected => | ||
setSelected(isSelected ? [...executionEnvironments] : []) | ||
} | ||
qsConfig={QS_CONFIG} | ||
additionalControls={[ | ||
...(canAdd | ||
? [ | ||
<ToolbarAddButton | ||
key="add" | ||
linkTo={`${match.url}/add`} | ||
/>, | ||
] | ||
: []), | ||
<ToolbarDeleteButton | ||
key="delete" | ||
onDelete={handleDelete} | ||
itemsToDelete={selected} | ||
pluralizedItemName={i18n._(t`Execution Environments`)} | ||
/>, | ||
]} | ||
/> | ||
)} | ||
renderItem={executionEnvironment => ( | ||
<ExecutionEnvironmentsListItem | ||
executionEnvironment={executionEnvironment} | ||
detailUrl={`${match.url}/${executionEnvironment.id}/details`} | ||
onSelect={() => handleSelect(executionEnvironment)} | ||
isSelected={selected.some( | ||
row => row.id === executionEnvironment.id | ||
)} | ||
/> | ||
)} | ||
emptyStateControls={ | ||
canAdd && ( | ||
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} /> | ||
) | ||
} | ||
/> | ||
</Card> | ||
</PageSection> | ||
<AlertModal | ||
aria-label={i18n._(t`Deletion error`)} | ||
isOpen={deletionError} | ||
onClose={clearDeletionError} | ||
title={i18n._(t`Error`)} | ||
variant="error" | ||
> | ||
{i18n._(t`Failed to delete one or more execution environments`)} | ||
<ErrorDetail error={deletionError} /> | ||
</AlertModal> | ||
</> | ||
); | ||
} | ||
|
||
export default ExecutionEnvironmentList; | ||
export default withI18n()(ExecutionEnvironmentList); |
Oops, something went wrong.