go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/frontend/ui/src/views/new_rule/new_rule.tsx (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import {
    16    ChangeEvent,
    17    useContext,
    18    useEffect,
    19    useState,
    20  } from 'react';
    21  import { useMutation } from 'react-query';
    22  import {
    23    useNavigate,
    24    useParams,
    25    useSearchParams,
    26  } from 'react-router-dom';
    27  
    28  import {
    29    GrpcError,
    30    RpcCode,
    31  } from '@chopsui/prpc-client';
    32  import LoadingButton from '@mui/lab/LoadingButton';
    33  import Backdrop from '@mui/material/Backdrop';
    34  import CircularProgress from '@mui/material/CircularProgress';
    35  import Container from '@mui/material/Container';
    36  import Grid from '@mui/material/Grid';
    37  import Paper from '@mui/material/Paper';
    38  import Typography from '@mui/material/Typography';
    39  import PanelHeading from '@/components/headings/panel_heading/panel_heading';
    40  
    41  import BugPicker from '@/components/bug_picker/bug_picker';
    42  import ErrorAlert from '@/components/error_alert/error_alert';
    43  import RuleEditInput from '@/components/rule_edit_input/rule_edit_input';
    44  import { SnackbarContext } from '@/context/snackbar_context';
    45  import { ClusterId } from '@/proto/go.chromium.org/luci/analysis/proto/v1/common.pb';
    46  import { CreateRuleRequest, Rule } from '@/proto/go.chromium.org/luci/analysis/proto/v1/rules.pb';
    47  import { linkToRule } from '@/tools/urlHandling/links';
    48  import { getRulesService } from '@/services/services';
    49  
    50  const NewRulePage = () => {
    51    const { project } = useParams();
    52    const navigate = useNavigate();
    53    const [searchParams] = useSearchParams();
    54  
    55    const [bugSystem, setBugSystem] = useState<string>('');
    56    const [bugId, setBugId] = useState<string>('');
    57    const [definition, setDefinition] = useState<string>('');
    58    const [sourceCluster, setSourceCluster] = useState<ClusterId>({ algorithm: '', id: '' });
    59  
    60    const { setSnack } = useContext(SnackbarContext);
    61    const createRule = useMutation((request: CreateRuleRequest) => service.create(request));
    62    const [validationError, setValidationError] = useState<GrpcError | null>(null);
    63  
    64    useEffect(() => {
    65      const rule = searchParams.get('rule');
    66      if (rule) {
    67        setDefinition(rule);
    68      }
    69      const sourceClusterAlg = searchParams.get('sourceAlg');
    70      const sourceClusterID = searchParams.get('sourceId');
    71      if (sourceClusterAlg && sourceClusterID) {
    72        setSourceCluster({
    73          algorithm: sourceClusterAlg,
    74          id: sourceClusterID,
    75        });
    76      }
    77    }, [searchParams]);
    78  
    79  
    80    if (!project) {
    81      return (
    82        <ErrorAlert
    83          errorTitle="Project not found"
    84          errorText="A project is required to create a rule, please check the URL and try again."
    85          showError
    86        />
    87      );
    88    }
    89  
    90    const service = getRulesService();
    91  
    92    const handleBugSystemChange = (bugSystem: string) => {
    93      setBugSystem(bugSystem);
    94    };
    95  
    96    const handleBugIdChange = (bugId: string) => {
    97      setBugId(bugId);
    98    };
    99  
   100    const handleRuleDefinitionChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
   101      setDefinition(e.target.value);
   102    };
   103  
   104  
   105    const handleSave = () => {
   106      const request: CreateRuleRequest = {
   107        parent: `projects/${project}`,
   108        rule: Rule.create({
   109          bug: {
   110            system: bugSystem,
   111            id: bugId,
   112          },
   113          ruleDefinition: definition,
   114          isActive: true,
   115          isManagingBug: false,
   116          isManagingBugPriority: true,
   117          sourceCluster: sourceCluster,
   118        }),
   119      };
   120      createRule.mutate(request, {
   121        onSuccess(data) {
   122          const rule = data;
   123          setSnack({
   124            open: true,
   125            message: 'Rule updated successfully',
   126            severity: 'success',
   127          });
   128          navigate(linkToRule(rule.project, rule.ruleId));
   129        },
   130        onError(error) {
   131          if ( error instanceof GrpcError &&
   132            error.code === RpcCode.INVALID_ARGUMENT) {
   133            setValidationError(error);
   134          } else {
   135            setSnack({
   136              open: true,
   137              message: `Failed to create rule due to: ${error}`,
   138              severity: 'error',
   139            });
   140          }
   141        },
   142      });
   143    };
   144  
   145    return (
   146      <Container>
   147        <Paper elevation={3} sx={{
   148          pt: 1,
   149          pb: 4,
   150          px: 2,
   151          mt: 1,
   152          mx: 2,
   153        }}>
   154          <PanelHeading>
   155            New Rule
   156          </PanelHeading>
   157          <Grid container direction='column' spacing={1}>
   158            <Grid item xs>
   159              {validationError && (
   160                <ErrorAlert
   161                  errorTitle="Validation error"
   162                  errorText={`Rule data is invalid: ${validationError.description.trim()}`}
   163                  showError
   164                  onErrorClose={() => setValidationError(null)}
   165                />
   166              )}
   167            </Grid>
   168            <Grid item container xs>
   169              <Grid item xs={6}>
   170                <Typography>
   171                  Associated bug
   172                </Typography>
   173                <BugPicker
   174                  bugSystem={bugSystem}
   175                  bugId={bugId}
   176                  handleBugSystemChanged={handleBugSystemChange}
   177                  handleBugIdChanged={handleBugIdChange} />
   178              </Grid>
   179            </Grid>
   180            <Grid item xs marginTop='1rem'>
   181              <Typography>
   182                Rule definition
   183              </Typography>
   184              <RuleEditInput
   185                definition={definition}
   186                onDefinitionChange={handleRuleDefinitionChange}
   187              />
   188            </Grid>
   189            <Grid item xs>
   190              <LoadingButton
   191                variant="contained"
   192                data-testid="create-rule-save"
   193                onClick={handleSave}
   194                loading={createRule.isLoading}>
   195                  Save
   196              </LoadingButton>
   197            </Grid>
   198          </Grid>
   199        </Paper>
   200        <Backdrop
   201          sx={{ color: '#fff' }}
   202          open={createRule.isSuccess}
   203        >
   204          <CircularProgress color="inherit" />
   205        </Backdrop>
   206      </Container>
   207    );
   208  };
   209  
   210  export default NewRulePage;