BootstrapStep: Enhancement resource manage card options (#110723)

* BootstrapStep: card reorganize
* ConnectRepositoryButton: Move to Provisioning page actions spot
This commit is contained in:
Yunwen Zheng 2025-09-09 12:23:06 -04:00 committed by GitHub
parent d5fca9a5fa
commit fc0db985c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 155 additions and 52 deletions

View File

@ -10,6 +10,7 @@ import { Page } from 'app/core/components/Page/Page';
import GettingStarted from './GettingStarted/GettingStarted'; import GettingStarted from './GettingStarted/GettingStarted';
import GettingStartedPage from './GettingStarted/GettingStartedPage'; import GettingStartedPage from './GettingStarted/GettingStartedPage';
import { ConnectRepositoryButton } from './Shared/ConnectRepositoryButton';
import { RepositoryList } from './Shared/RepositoryList'; import { RepositoryList } from './Shared/RepositoryList';
import { InlineSecureValueWarning } from './components/InlineSecureValueWarning'; import { InlineSecureValueWarning } from './components/InlineSecureValueWarning';
import { useRepositoryList } from './hooks/useRepositoryList'; import { useRepositoryList } from './hooks/useRepositoryList';
@ -67,6 +68,7 @@ export default function HomePage() {
<Page <Page
navId="provisioning" navId="provisioning"
subTitle={t('provisioning.home-page.subtitle', 'View and manage your configured repositories')} subTitle={t('provisioning.home-page.subtitle', 'View and manage your configured repositories')}
actions={activeTab === TabSelection.Repositories && <ConnectRepositoryButton items={items} />}
> >
<Page.Contents isLoading={isLoading}> <Page.Contents isLoading={isLoading}>
{settings.data?.legacyStorage && ( {settings.data?.legacyStorage && (

View File

@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom-v5-compat'; import { useNavigate } from 'react-router-dom-v5-compat';
import { Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
import { Alert, Button, Dropdown, Icon, Menu, Stack } from '@grafana/ui'; import { Button, Dropdown, Icon, Menu, Stack } from '@grafana/ui';
import { Repository } from 'app/api/clients/provisioning/v0alpha1'; import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1/endpoints.gen'; import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1/endpoints.gen';
@ -18,22 +18,7 @@ export function ConnectRepositoryButton({ items }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: frontendSettings } = useGetFrontendSettingsQuery(); const { data: frontendSettings } = useGetFrontendSettingsQuery();
if (state.instanceConnected) { const isButtonDisabled = state.instanceConnected || state.maxReposReached;
return null;
}
if (state.maxReposReached) {
return (
<Alert title="" severity="info">
<Trans
i18nKey="provisioning.connect-repository-button.repository-limit-info-alert"
values={{ count: state.repoCount }}
>
Repository limit reached ({'{{count}}'})
</Trans>
</Alert>
);
}
const availableTypes = frontendSettings?.availableRepositoryTypes || DEFAULT_REPOSITORY_TYPES; const availableTypes = frontendSettings?.availableRepositoryTypes || DEFAULT_REPOSITORY_TYPES;
const { orderedConfigs } = getOrderedRepositoryConfigs(availableTypes); const { orderedConfigs } = getOrderedRepositoryConfigs(availableTypes);
@ -55,7 +40,15 @@ export function ConnectRepositoryButton({ items }: Props) {
</Menu> </Menu>
} }
> >
<Button variant="primary"> <Button
variant="primary"
disabled={isButtonDisabled}
tooltip={getConfigureRepoTooltip({
instanceConnected: state.instanceConnected,
maxReposReached: state.maxReposReached,
count: state.repoCount,
})}
>
<Stack alignItems="center"> <Stack alignItems="center">
<Trans i18nKey="provisioning.connect-repository-button.configure">Configure</Trans> <Trans i18nKey="provisioning.connect-repository-button.configure">Configure</Trans>
<Icon name={'angle-down'} /> <Icon name={'angle-down'} />
@ -64,3 +57,32 @@ export function ConnectRepositoryButton({ items }: Props) {
</Dropdown> </Dropdown>
); );
} }
export function getConfigureRepoTooltip({
instanceConnected,
maxReposReached,
count,
}: {
instanceConnected: boolean;
maxReposReached: boolean;
count: number;
}) {
if (instanceConnected) {
return t(
'provisioning.connect-repository-button.instance-fully-managed-tooltip',
'Configuration is disabled because this instance is fully managed'
);
}
if (maxReposReached) {
return t(
'provisioning.connect-repository-button.repository-limit-reached-tooltip',
'Repository limit reached {{count}}',
{
count,
}
);
}
return '';
}

View File

@ -7,8 +7,6 @@ import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { RepositoryCard } from '../Repository/RepositoryCard'; import { RepositoryCard } from '../Repository/RepositoryCard';
import { checkSyncSettings } from '../utils/checkSyncSettings'; import { checkSyncSettings } from '../utils/checkSyncSettings';
import { ConnectRepositoryButton } from './ConnectRepositoryButton';
interface Props { interface Props {
items: Repository[]; items: Repository[];
} }
@ -27,7 +25,6 @@ export function RepositoryList({ items }: Props) {
value={query} value={query}
onChange={setQuery} onChange={setQuery}
/> />
<ConnectRepositoryButton items={items} />
</Stack> </Stack>
)} )}
<Stack direction={'column'} gap={2}> <Stack direction={'column'} gap={2}>

View File

@ -140,9 +140,8 @@ describe('BootstrapStep', () => {
it('should render correct info for GitHub repository type', async () => { it('should render correct info for GitHub repository type', async () => {
setup(); setup();
expect(await screen.findByText('Grafana instance')).toBeInTheDocument(); expect(screen.getAllByText('External storage')).toHaveLength(2);
expect(screen.getByText('External storage')).toBeInTheDocument(); expect(screen.getAllByText('Empty')).toHaveLength(3); // Three elements should have the role "Empty" (2 external + 1 unmanaged)
expect(screen.getAllByText('Empty')).toHaveLength(2); // Both should show empty
}); });
it('should render correct info for local file repository type', async () => { it('should render correct info for local file repository type', async () => {
@ -170,7 +169,7 @@ describe('BootstrapStep', () => {
setup(); setup();
expect(await screen.findByText('2 files')).toBeInTheDocument(); expect(await screen.getAllByText('2 files')).toHaveLength(2);
}); });
it('should display resource counts when resources exist', async () => { it('should display resource counts when resources exist', async () => {

View File

@ -1,11 +1,15 @@
import { css } from '@emotion/css';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n'; import { GrafanaTheme2 } from '@grafana/data';
import { Box, Card, Field, Input, LoadingPlaceholder, Stack, Text } from '@grafana/ui'; import { t } from '@grafana/i18n';
import { Box, Card, Field, Input, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui';
import { RepositoryViewList } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryViewList } from 'app/api/clients/provisioning/v0alpha1';
import { generateRepositoryTitle } from 'app/features/provisioning/utils/data'; import { generateRepositoryTitle } from 'app/features/provisioning/utils/data';
import { BootstrapStepCardIcons } from './BootstrapStepCardIcons';
import { BootstrapStepResourceCounting } from './BootstrapStepResourceCounting';
import { useStepStatus } from './StepStatusContext'; import { useStepStatus } from './StepStatusContext';
import { useModeOptions } from './hooks/useModeOptions'; import { useModeOptions } from './hooks/useModeOptions';
import { useResourceStats } from './hooks/useResourceStats'; import { useResourceStats } from './hooks/useResourceStats';
@ -28,9 +32,11 @@ export function BootstrapStep({ settingsData, repoName }: Props) {
} = useFormContext<WizardFormData>(); } = useFormContext<WizardFormData>();
const selectedTarget = watch('repository.sync.target'); const selectedTarget = watch('repository.sync.target');
const repositoryType = watch('repository.type');
const options = useModeOptions(repoName, settingsData); const options = useModeOptions(repoName, settingsData);
const { target } = options[0]; const { target } = options[0];
const { resourceCountString, fileCountString, isLoading } = useResourceStats(repoName, settingsData?.legacyStorage); const { resourceCountString, fileCountString, isLoading } = useResourceStats(repoName, settingsData?.legacyStorage);
const styles = useStyles2(getStyles);
useEffect(() => { useEffect(() => {
// Pick a name nice name based on type+settings // Pick a name nice name based on type+settings
@ -60,25 +66,6 @@ export function BootstrapStep({ settingsData, repoName }: Props) {
return ( return (
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<Box alignItems="center" padding={4}>
<Stack direction="row" gap={4} alignItems="flex-start" justifyContent="center">
<Stack direction="column" gap={1} alignItems="center">
<Text color="secondary">
<Trans i18nKey="provisioning.bootstrap-step.grafana">Grafana instance</Trans>
</Text>
<Stack direction="row" gap={2}>
<Text variant="h4">{resourceCountString}</Text>
</Stack>
</Stack>
<Stack direction="column" gap={1} alignItems="center">
<Text color="secondary">
<Trans i18nKey="provisioning.bootstrap-step.ext-storage">External storage</Trans>
</Text>
<Text variant="h4">{fileCountString}</Text>
</Stack>
</Stack>
</Box>
<Controller <Controller
name="repository.sync.target" name="repository.sync.target"
control={control} control={control}
@ -94,12 +81,27 @@ export function BootstrapStep({ settingsData, repoName }: Props) {
noMargin noMargin
{...field} {...field}
> >
<Card.Heading>{action.label}</Card.Heading> <Card.Heading>
<Text variant="h5">{action.label}</Text>
</Card.Heading>
<Card.Description> <Card.Description>
<div className={styles.divider} />
<Box paddingBottom={2}>
<BootstrapStepCardIcons target={action.target} repoType={repositoryType} />
</Box>
<Stack direction="column" gap={3}> <Stack direction="column" gap={3}>
{action.description} {action.description}
<Text color="primary">{action.subtitle}</Text> <Text color="primary">{action.subtitle}</Text>
</Stack> </Stack>
<div className={styles.divider} />
<BootstrapStepResourceCounting
target={action.target}
fileCountString={fileCountString}
resourceCountString={resourceCountString}
/>
</Card.Description> </Card.Description>
</Card> </Card>
))} ))}
@ -138,3 +140,13 @@ export function BootstrapStep({ settingsData, repoName }: Props) {
</Stack> </Stack>
); );
} }
const getStyles = (theme: GrafanaTheme2) => ({
divider: css({
height: 1,
width: '100%',
backgroundColor: theme.colors.border.medium,
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
}),
});

View File

@ -0,0 +1,29 @@
import { Icon, Stack } from '@grafana/ui';
import { RepoIcon } from '../Shared/RepoIcon';
import { RepoType, Target } from './types';
export function BootstrapStepCardIcons({ target, repoType }: { target: Target; repoType: RepoType }) {
if (target === 'instance') {
return (
<Stack direction="row">
<Icon name="grafana" size="xxl" />
<Icon name="arrows-h" size="xxl" />
<RepoIcon type="github" />
</Stack>
);
}
if (target === 'folder') {
return (
<Stack>
<Icon name="folder" size="xxl" />
<Icon name="arrow-left" size="xxl" />
<RepoIcon type={repoType} />
</Stack>
);
}
return null;
}

View File

@ -0,0 +1,40 @@
import { Trans } from '@grafana/i18n';
import { Stack, Text } from '@grafana/ui';
import { Target } from './types';
export function BootstrapStepResourceCounting({
target,
fileCountString,
resourceCountString,
}: {
target: Target;
fileCountString: string;
resourceCountString: string;
}) {
if (target === 'instance') {
return (
<Stack direction="row" gap={3}>
<Stack gap={1}>
<Trans i18nKey="provisioning.bootstrap-step.external-storage-label">External storage</Trans>
<Text color="primary">{fileCountString}</Text>
</Stack>
<Stack gap={1}>
<Trans i18nKey="provisioning.bootstrap-step.unmanaged-resources-label">Unmanaged resources</Trans>{' '}
<Text color="primary">{resourceCountString}</Text>
</Stack>
</Stack>
);
}
if (target === 'folder') {
return (
<Stack gap={1}>
<Trans i18nKey="provisioning.bootstrap-step.external-storage-label">External storage</Trans>{' '}
<Text color="primary">{fileCountString}</Text>
</Stack>
);
}
return null;
}

View File

@ -11215,15 +11215,15 @@
"description-clear-repository-connection": "Add a clear name for this repository connection", "description-clear-repository-connection": "Add a clear name for this repository connection",
"empty": "Empty", "empty": "Empty",
"error-field-required": "This field is required.", "error-field-required": "This field is required.",
"ext-storage": "External storage", "external-storage-label": "External storage",
"files-count_one": "{{count}} files", "files-count_one": "{{count}} files",
"files-count_other": "{{count}} files", "files-count_other": "{{count}} files",
"folders-count_one": "{{count}} folder", "folders-count_one": "{{count}} folder",
"folders-count_other": "{{count}} folder", "folders-count_other": "{{count}} folder",
"grafana": "Grafana instance",
"label-display-name": "Display name", "label-display-name": "Display name",
"placeholder-my-repository-connection": "My repository connection", "placeholder-my-repository-connection": "My repository connection",
"text-loading-resource-information": "Loading resource information..." "text-loading-resource-information": "Loading resource information...",
"unmanaged-resources-label": "Unmanaged resources"
}, },
"check-repository": { "check-repository": {
"check": "Check" "check": "Check"
@ -11265,7 +11265,9 @@
}, },
"connect-repository-button": { "connect-repository-button": {
"configure": "Configure", "configure": "Configure",
"repository-limit-info-alert": "Repository limit reached ({{count}})" "instance-fully-managed-tooltip": "Configuration is disabled because this instance is fully managed",
"repository-limit-reached-tooltip_one": "Repository limit reached {{count}}",
"repository-limit-reached-tooltip_other": "Repository limit reached {{count}}"
}, },
"delete-repository-button": { "delete-repository-button": {
"button-delete": "Delete", "button-delete": "Delete",