go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/test_verdict/legacy/invocation_page/invocation_page.tsx (about)

     1  // Copyright 2023 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 { LinearProgress } from '@mui/material';
    16  import { observer } from 'mobx-react-lite';
    17  import { useEffect } from 'react';
    18  import { useParams } from 'react-router-dom';
    19  
    20  import { RecoverableErrorBoundary } from '@/common/components/error_handling';
    21  import { PageMeta } from '@/common/components/page_meta/page_meta';
    22  import { AppRoutedTab, AppRoutedTabs } from '@/common/components/routed_tabs';
    23  import { INVOCATION_STATE_DISPLAY_MAP } from '@/common/constants/legacy';
    24  import { useStore } from '@/common/store';
    25  import {
    26    getBuildURLPathFromBuildId,
    27    getSwarmingTaskURL,
    28  } from '@/common/tools/url_utils';
    29  
    30  import { CountIndicator } from '../test_results_tab/count_indicator';
    31  
    32  import { InvLitEnvProvider } from './inv_lit_env_provider';
    33  
    34  // Should be checked upstream, but allowlist URLs here just to be safe.
    35  const ALLOWED_SWARMING_HOSTS = [
    36    'chromium-swarm-dev.appspot.com',
    37    'chromium-swarm.appspot.com',
    38    'chrome-swarming.appspot.com',
    39  ];
    40  
    41  export const InvocationPage = observer(() => {
    42    const { invId } = useParams();
    43    const store = useStore();
    44  
    45    if (!invId) {
    46      throw new Error('invariant violated: invId should be set');
    47    }
    48  
    49    useEffect(() => {
    50      store.invocationPage.setInvocationId(invId);
    51    }, [invId, store]);
    52  
    53    const inv = store.invocationPage.invocation.invocation;
    54    const project = store.invocationPage.invocation.project;
    55    const buildId = invId.match(/^build-(?<id>\d+)/)?.groups?.['id'];
    56    const { swarmingHost, taskId } =
    57      invId.match(/^task-(?<swarmingHost>.*)-(?<taskId>[0-9a-fA-F]+)$/)?.groups ||
    58      {};
    59  
    60    return (
    61      <InvLitEnvProvider>
    62        <PageMeta project={project || ''} title={`inv: ${invId}`} />
    63        <div
    64          css={{
    65            backgroundColor: 'var(--block-background-color)',
    66            padding: '6px 16px',
    67            display: 'flex',
    68          }}
    69        >
    70          <div css={{ flex: '0 auto' }}>
    71            <span css={{ color: 'var(--light-text-color)' }}>Invocation ID </span>
    72            <span>{invId}</span>
    73            {buildId && (
    74              <>
    75                {' '}
    76                (
    77                <a
    78                  href={getBuildURLPathFromBuildId(buildId)}
    79                  target="_blank"
    80                  rel="noreferrer"
    81                >
    82                  build page
    83                </a>
    84                )
    85              </>
    86            )}
    87            {ALLOWED_SWARMING_HOSTS.includes(swarmingHost) && taskId && (
    88              <a
    89                href={getSwarmingTaskURL(swarmingHost, taskId)}
    90                target="_blank"
    91                rel="noreferrer"
    92              >
    93                task page
    94              </a>
    95            )}
    96          </div>
    97          <div
    98            css={{
    99              marginLeft: 'auto',
   100              flex: '0 auto',
   101            }}
   102          >
   103            {inv && (
   104              <>
   105                <i>{INVOCATION_STATE_DISPLAY_MAP[inv.state]}</i>
   106                {inv.finalizeTime ? (
   107                  <> at {new Date(inv.finalizeTime).toLocaleString()}</>
   108                ) : (
   109                  <> since {new Date(inv.createTime).toLocaleString()}</>
   110                )}
   111              </>
   112            )}
   113          </div>
   114        </div>
   115        <LinearProgress
   116          value={100}
   117          variant={inv ? 'determinate' : 'indeterminate'}
   118        />
   119        <AppRoutedTabs>
   120          <AppRoutedTab
   121            label="Test Results"
   122            value="test-results"
   123            to="test-results"
   124            icon={<CountIndicator />}
   125            iconPosition="end"
   126          />
   127          <AppRoutedTab
   128            label="Invocation Details"
   129            value="invocation-details"
   130            to="invocation-details"
   131          />
   132        </AppRoutedTabs>
   133      </InvLitEnvProvider>
   134    );
   135  });
   136  
   137  export const element = (
   138    // See the documentation for `<LoginPage />` for why we handle error this way.
   139    <RecoverableErrorBoundary key="invocation">
   140      <InvocationPage />
   141    </RecoverableErrorBoundary>
   142  );