peridot/peridot/ui/src/components/ProjectPackages.tsx

414 lines
12 KiB
TypeScript

/*
* Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
* Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
* Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import React from 'react';
import Alert from '@mui/material/Alert';
import TextField from '@mui/material/TextField';
import SearchIcon from '@mui/icons-material/Search';
import {
DataGrid,
GridColDef,
GridColumns,
GridRowsProp,
GridSelectionModel,
GridRenderCellParams,
} from '@mui/x-data-grid';
import { PageWrapper } from 'dotui/PageWrapper';
import { Table, TableCol, TableRow } from 'dotui/Table';
import {
V1ListPackagesResponse,
V1AsyncTask,
V1PackageType,
} from 'bazel-bin/peridot/proto/v1/client_typescript';
import { fetchRemoteResource, suspenseRemoteResource } from 'common/ui/remote';
import { buildApi, importApi, packageApi } from 'peridot/ui/src/api';
import { ProjectContext } from 'peridot/ui/src/context/ProjectContext';
import { Link } from 'react-router-dom';
import { PeridotLink } from 'common/ui/PeridotLink';
import { ToolbarHeader } from 'common/mui/ToolbarHeader';
import { RemoteErrors } from 'common/ui/types';
import Snackbar from '@mui/material/Snackbar';
import Toolbar from '@mui/material/Toolbar';
import Divider from '@mui/material/Divider';
import Button from '@mui/material/Button';
import { reqap } from 'common/ui/reqap';
import to from 'await-to-js';
import Stack from '@mui/material/Stack';
import Chip from '@mui/material/Chip';
import { Header } from 'common/mui/Header';
import InputAdornment from '@mui/material/InputAdornment';
const columns: GridColDef[] = [
{
field: 'id',
headerName: 'Name',
sortable: false,
flex: 1,
renderCell: (params) => (
<PeridotLink to={`/packages/${params.row['name']}`}>
{params.row['name']}
</PeridotLink>
),
},
{
field: 'tags',
headerName: 'Tags',
sortable: false,
flex: 1,
renderCell: (params: GridRenderCellParams<string>) => {
return (
<Stack direction="row" spacing={1}>
{(params.row['type'] === V1PackageType.NormalFork ||
params.row['type'] === V1PackageType.NormalSrc ||
params.row['type'] === V1PackageType.NormalForkModuleComponent ||
params.row['type'] === V1PackageType.NormalForkModule) && (
<Chip size="small" label="Package" variant="outlined" />
)}
{(params.row['type'] === V1PackageType.ModuleFork ||
params.row['type'] === V1PackageType.NormalForkModule ||
params.row['type'] === V1PackageType.ModuleForkModuleComponent) && (
<Chip
size="small"
label="Module"
color="success"
variant="outlined"
/>
)}
{(params.row['type'] === V1PackageType.ModuleForkComponent ||
params.row['type'] === V1PackageType.ModuleForkModuleComponent) && (
<Chip
size="small"
label="Part of module"
color="primary"
variant="outlined"
/>
)}
</Stack>
);
},
},
{
field: 'lastImportAt',
headerName: 'Last import',
sortable: false,
flex: 1,
renderCell: (params: GridRenderCellParams<string | null>) => (
<Chip
size="small"
variant="outlined"
label={params.value ? new Date(params.value).toLocaleString() : 'Never'}
color={params.value ? 'success' : 'error'}
/>
),
},
{
field: 'lastBuildAt',
headerName: 'Last build',
sortable: false,
flex: 1,
renderCell: (params: GridRenderCellParams<string | null>) => (
<Chip
size="small"
variant="outlined"
label={params.value ? new Date(params.value).toLocaleString() : 'Never'}
color={params.value ? 'success' : 'error'}
/>
),
},
];
export default function () {
const project = React.useContext(ProjectContext);
const [submitting, setSubmitting] = React.useState(false);
const [buildDisabled, setBuildDisabled] = React.useState(false);
const [importDisabled, setImportDisabled] = React.useState(false);
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
const [snackbarMessage, setSnackbarMessage] = React.useState('');
const [snackbarLink, setSnackbarLink] = React.useState<string | null>(null);
const [snackbarSeverity, setSnackbarSeverity] = React.useState<
'success' | 'error'
>('success');
const [buildPackageIds, setBuildPackageIds] = React.useState<string[]>([]);
const [nameFilter, setNameFilter] = React.useState<string | undefined>();
const [searchTimeout, setSearchTimeout] =
React.useState<NodeJS.Timeout | null>(null);
const [packagesRes, setPackagesRes] = React.useState<
V1ListPackagesResponse | RemoteErrors | undefined | null
>(undefined);
const [pageSize, setPageSize] = React.useState(100);
const [page, setPage] = React.useState(0);
fetchRemoteResource(
() =>
packageApi.listPackages({
projectId: project?.id || '',
filtersName: nameFilter,
limit: pageSize,
page: page,
}),
setPackagesRes,
false,
[nameFilter, pageSize, page]
);
const handleClose = () => {
setSnackbarOpen(false);
};
const openSnackbar = (
message: string,
severity: 'success' | 'error',
link: string | null
) => {
setSnackbarMessage(message);
setSnackbarSeverity(severity);
setSnackbarLink(link);
setSnackbarOpen(true);
};
const onSelectionModelChange = (sm: GridSelectionModel) => {
setImportDisabled(false);
setBuildDisabled(false);
setBuildPackageIds(sm.map((x) => x.toString()));
};
const doNameSearch = (val: string | null) => {
if (!val || val.trim().length === 0) {
setNameFilter(undefined);
return;
}
setNameFilter(val);
};
const nameSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e || !e.currentTarget) {
return;
}
if (searchTimeout) {
clearTimeout(searchTimeout);
}
const val = e.currentTarget.value;
setSearchTimeout(
setTimeout(() => {
doNameSearch(val);
}, 200)
);
};
const triggerImport = async () => {
setSubmitting(true);
setImportDisabled(true);
let err, res;
if (buildPackageIds.length === 1) {
[err, res] = await to(
importApi.importPackage({
projectId: project?.id || '',
body: {
packageId: buildPackageIds[0],
},
})
);
if (!err && res) {
openSnackbar('Import triggered', 'success', `tasks/${res.taskId}`);
}
} else {
[err, res] = await to(
importApi.importPackageBatch({
projectId: project?.id || '',
body: {
imports: buildPackageIds.map((x) => ({ packageId: x })),
},
})
);
if (!err && res) {
openSnackbar(
'Imports triggered',
'success',
`import_batches/${res.importBatchId}`
);
}
}
if (err || !res) {
openSnackbar('Could not trigger import(s)', 'error', null);
setSubmitting(false);
return;
}
setSubmitting(false);
};
const triggerBuild = async () => {
setSubmitting(true);
setBuildDisabled(true);
let err, res;
if (buildPackageIds.length === 1) {
[err, res] = await to(
buildApi.submitBuild({
projectId: project?.id || '',
body: {
packageId: buildPackageIds[0],
},
})
);
if (!err && res) {
openSnackbar('Build triggered', 'success', `tasks/${res.taskId}`);
}
} else {
[err, res] = await to(
buildApi.submitBuildBatch({
projectId: project?.id || '',
body: {
builds: buildPackageIds.map((x) => ({
packageId: x,
})),
},
})
);
if (!err && res) {
openSnackbar(
'Builds triggered',
'success',
`build_batches/${res.buildBatchId}`
);
}
}
if (err || !res) {
openSnackbar('Could not trigger build(s)', 'error', null);
setSubmitting(false);
return;
}
setSubmitting(false);
};
return (
<>
<Header title="Packages" />
<Divider />
<Toolbar className="bg-gray-100 flex space-x-6 py-3">
<TextField
fullWidth
className="h-full"
label="Search"
type="search"
size="small"
onChange={nameSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
{window.state.email && (
<>
<Button
variant="contained"
disabled={
buildPackageIds.length === 0 || submitting || importDisabled
}
onClick={triggerImport}
>
Import
</Button>
<Button
variant="contained"
disabled={
buildPackageIds.length === 0 || submitting || buildDisabled
}
onClick={triggerBuild}
>
Build
</Button>
</>
)}
</Toolbar>
<Snackbar
open={snackbarOpen}
onClose={handleClose}
autoHideDuration={5000}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert sx={{ width: '100%' }} severity={snackbarSeverity}>
<div className="flex space-x-2">
<div>{snackbarMessage}</div>
{snackbarLink && (
<div>
<PeridotLink to={`/${snackbarLink}`}>Details</PeridotLink>
</div>
)}
</div>
</Alert>
</Snackbar>
{suspenseRemoteResource(packagesRes, (res: V1ListPackagesResponse) => {
return (
res.packages && (
<DataGrid
autoHeight
pagination
checkboxSelection={!!window.state.email}
disableSelectionOnClick
disableDensitySelector
disableColumnSelector
disableColumnMenu
className="bg-white"
columns={columns}
density="compact"
rows={res.packages}
rowsPerPageOptions={[10, 25, 50, 100]}
rowCount={parseInt(res.total || '0')}
paginationMode="server"
pageSize={pageSize}
page={page}
onPageChange={(page) => setPage(page)}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
onSelectionModelChange={onSelectionModelChange}
selectionModel={buildPackageIds}
/>
)
);
})}
</>
);
}