[Provisioning] Display instance connection directly in home (#101720)
* Show tabs directly in home page for instance sync * Display actions also in the home page * Add delete button to actions * Fix issue with files tab * Display tabs also in instant sync * Generate connection name for instance sync * Fix issues when no repositories * Set active tab * Remove leftover * Rename component for listing folder repository * Fix linting issues * Fix merge error * Rename to HomePage * Split folder list into separate component * Create own component for repository card * Improve RepositoryCard readability * Improve RepositoryCard styling * Make view primary button * Fix syntax * Fix generateName
This commit is contained in:
1 parent
c6a4ca4d41
commit
bf591b2947
11 files changed
+569
-422
No files matched your search
+34
-26
@@ -5697,6 +5697,18 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"]
|
||||
],
|
||||
"public/app/features/provisioning/FilesView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
|
||||
],
|
||||
"public/app/features/provisioning/FolderRepositoryList.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/GettingStarted/EnhancedFeatures.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
@@ -5732,6 +5744,14 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/HomePage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/provisioning/JobStatus.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
@@ -5759,26 +5779,23 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/provisioning/RepositoryActions.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/provisioning/RepositoryCard.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/RepositoryHealth.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/provisioning/RepositoryListPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"]
|
||||
],
|
||||
"public/app/features/provisioning/RepositoryOverview.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
@@ -5811,19 +5828,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "15"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
|
||||
],
|
||||
"public/app/features/provisioning/SyncRepository.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
CellProps,
|
||||
Column,
|
||||
ConfirmModal,
|
||||
FilterInput,
|
||||
InteractiveTable,
|
||||
LinkButton,
|
||||
Spinner,
|
||||
Stack,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { Repository, useGetRepositoryFilesQuery, useDeleteRepositoryFilesWithPathMutation } from './api';
|
||||
import { FileDetails } from './api/types';
|
||||
import { PROVISIONING_URL } from './constants';
|
||||
|
||||
interface FilesViewProps {
|
||||
repo: Repository;
|
||||
}
|
||||
|
||||
type FileCell<T extends keyof FileDetails = keyof FileDetails> = CellProps<FileDetails, FileDetails[T]>;
|
||||
|
||||
export function FilesView({ repo }: FilesViewProps) {
|
||||
const name = repo.metadata?.name ?? '';
|
||||
const query = useGetRepositoryFilesQuery({ name });
|
||||
const [deleteFile, deleteFileStatus] = useDeleteRepositoryFilesWithPathMutation();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [pathToDelete, setPathToDelete] = useState<string>();
|
||||
const data = [...(query.data?.items ?? [])].filter((file) =>
|
||||
file.path.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const columns: Array<Column<FileDetails>> = [
|
||||
{
|
||||
id: 'path',
|
||||
header: 'Path',
|
||||
sortType: 'string',
|
||||
cell: ({ row: { original } }: FileCell<'path'>) => {
|
||||
const { path } = original;
|
||||
return <a href={`${PROVISIONING_URL}/${name}/file/${path}`}>{path}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
header: 'Size (KB)',
|
||||
cell: ({ row: { original } }: FileCell<'size'>) => {
|
||||
const { size } = original;
|
||||
return (parseInt(size, 10) / 1024).toFixed(2);
|
||||
},
|
||||
sortType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'hash',
|
||||
header: 'Hash',
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row: { original } }: FileCell<'path'>) => {
|
||||
const { path } = original;
|
||||
return (
|
||||
<Stack>
|
||||
{(path.endsWith('.json') || path.endsWith('.yaml') || path.endsWith('.yml')) && (
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/file/${path}`}>View</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/history/${path}`}>History</LinkButton>
|
||||
<Button variant="destructive" onClick={() => setPathToDelete(path)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<Stack justifyContent={'center'} alignItems={'center'}>
|
||||
<Spinner />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack grow={1} direction={'column'} gap={2}>
|
||||
<ConfirmModal
|
||||
isOpen={Boolean(pathToDelete?.length) || deleteFileStatus.isLoading}
|
||||
title="Delete file in repository?"
|
||||
body={deleteFileStatus.isLoading ? 'Deleting file...' : pathToDelete}
|
||||
confirmText="Delete"
|
||||
icon={deleteFileStatus.isLoading ? `spinner` : `exclamation-triangle`}
|
||||
onConfirm={() => {
|
||||
deleteFile({
|
||||
name: name,
|
||||
path: pathToDelete!,
|
||||
message: `Deleted from repo test UI`,
|
||||
});
|
||||
setPathToDelete('');
|
||||
}}
|
||||
onDismiss={() => setPathToDelete('')}
|
||||
/>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" autoFocus={true} value={searchQuery} onChange={setSearchQuery} />
|
||||
</Stack>
|
||||
<InteractiveTable columns={columns} data={data} pageSize={25} getRowId={(f: FileDetails) => String(f.path)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EmptySearchResult, FilterInput, LinkButton, Stack } from '@grafana/ui';
|
||||
|
||||
import { RepositoryCard } from './RepositoryCard';
|
||||
import { Repository, useGetFrontendSettingsQuery } from './api';
|
||||
import { CONNECT_URL } from './constants';
|
||||
import { checkSyncSettings } from './utils';
|
||||
|
||||
export function FolderRepositoryList({ items }: { items: Repository[] }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const filteredItems = items.filter((item) => item.metadata?.name?.includes(query));
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
const [instanceConnected] = checkSyncSettings(settings.data);
|
||||
|
||||
return (
|
||||
<Stack direction={'column'} gap={3}>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" value={query} onChange={setQuery} />
|
||||
{!instanceConnected && (
|
||||
<LinkButton href={CONNECT_URL} variant="primary" icon={'plus'}>
|
||||
Connect to repository
|
||||
</LinkButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction={'column'}>
|
||||
{!!filteredItems.length ? (
|
||||
filteredItems.map((item) => <RepositoryCard key={item.metadata?.name} repository={item} />)
|
||||
) : (
|
||||
<EmptySearchResult>No results matching your query </EmptySearchResult>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Alert, ConfirmModal, Modal, Stack, Tab, TabContent, TabsBar } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { ExportToRepository } from './ExportToRepository';
|
||||
import { FilesView } from './FilesView';
|
||||
import { FolderRepositoryList } from './FolderRepositoryList';
|
||||
import GettingStarted from './GettingStarted/GettingStarted';
|
||||
import GettingStartedPage from './GettingStarted/GettingStartedPage';
|
||||
import { MigrateToRepository } from './MigrateToRepository';
|
||||
import { RepositoryActions } from './RepositoryActions';
|
||||
import { RepositoryOverview } from './RepositoryOverview';
|
||||
import { RepositoryResources } from './RepositoryResources';
|
||||
import { useDeletecollectionRepositoryMutation, useGetFrontendSettingsQuery } from './api';
|
||||
import { useRepositoryList } from './hooks';
|
||||
import { checkSyncSettings } from './utils';
|
||||
|
||||
const appEvents = getAppEvents();
|
||||
|
||||
enum TabSelection {
|
||||
Overview = 'overview',
|
||||
Resources = 'resources',
|
||||
Files = 'files',
|
||||
GettingStarted = 'getting-started',
|
||||
Repositories = 'repositories',
|
||||
}
|
||||
|
||||
const connectedTabInfo = [
|
||||
{ value: TabSelection.Overview, label: 'Overview', title: 'Repository overview' },
|
||||
{ value: TabSelection.Resources, label: 'Resources', title: 'Resources saved in Grafana database' },
|
||||
{ value: TabSelection.Files, label: 'Files', title: 'The raw file list from the repository' },
|
||||
{ value: TabSelection.GettingStarted, label: 'Getting started', title: 'Getting started' },
|
||||
];
|
||||
|
||||
const disconnectedTabInfo = [
|
||||
{ value: TabSelection.Repositories, label: 'Repositories', title: 'List of repositories' },
|
||||
{ value: TabSelection.GettingStarted, label: 'Getting started', title: 'Getting started' },
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const [items, isLoading] = useRepositoryList({ watch: true });
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
const [deleteAll, deleteAllResult] = useDeletecollectionRepositoryMutation();
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [showMigrateModal, setShowMigrateModal] = useState(false);
|
||||
const [instanceConnected] = checkSyncSettings(settings.data);
|
||||
const [activeTab, setActiveTab] = useState<TabSelection>(
|
||||
instanceConnected ? TabSelection.Overview : TabSelection.Repositories
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteAllResult.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['All configured repositories deleted'],
|
||||
});
|
||||
}
|
||||
}, [deleteAllResult.isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(instanceConnected ? TabSelection.Overview : TabSelection.Repositories);
|
||||
}, [instanceConnected]);
|
||||
|
||||
// Early return for onboarding
|
||||
if (!items?.length && !isLoading) {
|
||||
return <GettingStartedPage />;
|
||||
}
|
||||
|
||||
const onConfirmDelete = () => {
|
||||
deleteAll({});
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
if (!instanceConnected) {
|
||||
switch (activeTab) {
|
||||
case TabSelection.Repositories:
|
||||
return <FolderRepositoryList items={items ?? []} />;
|
||||
case TabSelection.GettingStarted:
|
||||
return <GettingStarted />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const repo = items?.[0];
|
||||
if (!repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case TabSelection.Overview:
|
||||
return <RepositoryOverview repo={repo} />;
|
||||
case TabSelection.Resources:
|
||||
return <RepositoryResources repo={repo} />;
|
||||
case TabSelection.Files:
|
||||
return <FilesView repo={repo} />;
|
||||
case TabSelection.GettingStarted:
|
||||
return <GettingStarted />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
subTitle="View and manage your configured repositories"
|
||||
actions={
|
||||
instanceConnected && items?.length ? (
|
||||
<RepositoryActions
|
||||
repository={items[0]}
|
||||
showMigrateButton={settings.data?.legacyStorage}
|
||||
onExportClick={() => setShowExportModal(true)}
|
||||
onMigrateClick={() => setShowMigrateModal(true)}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
{settings.data?.legacyStorage && (
|
||||
<Alert
|
||||
title="Legacy storage detected"
|
||||
severity="error"
|
||||
buttonContent={<>Remove all configured repositories</>}
|
||||
onRemove={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
Configured repositories will not work while running legacy storage.
|
||||
</Alert>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={showDeleteModal}
|
||||
title="Delete all configured repositories"
|
||||
body="Are you sure you want to delete all configured repositories? This action cannot be undone."
|
||||
confirmText="Delete repositories"
|
||||
onConfirm={onConfirmDelete}
|
||||
onDismiss={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
{showExportModal && items?.length && (
|
||||
<Modal isOpen={true} title="Export to Repository" onDismiss={() => setShowExportModal(false)}>
|
||||
<ExportToRepository repo={items[0]} />
|
||||
</Modal>
|
||||
)}
|
||||
{showMigrateModal && items?.length && (
|
||||
<Modal isOpen={true} title="Migrate to Repository" onDismiss={() => setShowMigrateModal(false)}>
|
||||
<MigrateToRepository repo={items[0]} />
|
||||
</Modal>
|
||||
)}
|
||||
<Stack direction="column" gap={2}>
|
||||
<TabsBar>
|
||||
{(instanceConnected ? connectedTabInfo : disconnectedTabInfo).map((t) => (
|
||||
<Tab
|
||||
key={t.value}
|
||||
label={t.label}
|
||||
active={activeTab === t.value}
|
||||
onChangeTab={() => setActiveTab(t.value)}
|
||||
title={t.title}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent>{renderTabContent()}</TabContent>
|
||||
</Stack>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Button, LinkButton, Stack } from '@grafana/ui';
|
||||
|
||||
import { DeleteRepositoryButton } from './DeleteRepositoryButton';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { SyncRepository } from './SyncRepository';
|
||||
import { Repository } from './api';
|
||||
import { PROVISIONING_URL } from './constants';
|
||||
import { getRemoteURL } from './utils/git';
|
||||
|
||||
interface RepositoryActionsProps {
|
||||
repository: Repository;
|
||||
showMigrateButton?: boolean;
|
||||
onExportClick?: () => void;
|
||||
onMigrateClick?: () => void;
|
||||
}
|
||||
|
||||
export function RepositoryActions({
|
||||
repository,
|
||||
showMigrateButton,
|
||||
onExportClick,
|
||||
onMigrateClick,
|
||||
}: RepositoryActionsProps) {
|
||||
const remoteURL = getRemoteURL(repository);
|
||||
const name = repository.metadata?.name ?? '';
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<StatusBadge
|
||||
enabled={Boolean(repository.spec?.sync?.enabled)}
|
||||
state={repository.status?.sync?.state}
|
||||
name={name}
|
||||
/>
|
||||
{remoteURL && (
|
||||
<Button variant="secondary" icon="github" onClick={() => window.open(remoteURL, '_blank')}>
|
||||
Source Code
|
||||
</Button>
|
||||
)}
|
||||
<SyncRepository repository={repository} />
|
||||
{showMigrateButton ? (
|
||||
<Button variant="secondary" icon="cloud-upload" onClick={onMigrateClick}>
|
||||
Migrate
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" icon="cloud-upload" onClick={onExportClick}>
|
||||
Push
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`}>
|
||||
Settings
|
||||
</LinkButton>
|
||||
<DeleteRepositoryButton name={name} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { IconName, Stack, Text, TextLink, Icon, Card, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { DeleteRepositoryButton } from './DeleteRepositoryButton';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { SyncRepository } from './SyncRepository';
|
||||
import { Repository, ResourceCount } from './api';
|
||||
import { PROVISIONING_URL } from './constants';
|
||||
|
||||
interface Props {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export function RepositoryCard({ repository }: Props) {
|
||||
const { metadata, spec, status } = repository;
|
||||
const name = metadata?.name ?? '';
|
||||
|
||||
const getRepositoryMeta = (): ReactNode[] => {
|
||||
const meta: ReactNode[] = [];
|
||||
|
||||
if (spec?.type === 'github') {
|
||||
const { url = '', branch } = spec.github ?? {};
|
||||
const branchUrl = branch ? `${url}/tree/${branch}` : url;
|
||||
|
||||
meta.push(
|
||||
<TextLink key="link" external href={branchUrl}>
|
||||
{branchUrl}
|
||||
</TextLink>
|
||||
);
|
||||
|
||||
if (status?.webhook?.id) {
|
||||
const webhookUrl = `${url}/settings/hooks/${status.webhook.id}`;
|
||||
meta.push(
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
<TextLink key="webhook" href={webhookUrl}>
|
||||
Webhook
|
||||
</TextLink>
|
||||
<Icon name="check" className="text-success" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
} else if (spec?.type === 'local') {
|
||||
meta.push(
|
||||
<Text variant="bodySmall" key="path">
|
||||
{spec.local?.path ?? ''}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return meta;
|
||||
};
|
||||
|
||||
const getRepositoryIcon = (): IconName => {
|
||||
return spec?.type === 'github' ? 'github' : 'database';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={name}>
|
||||
<Card.Figure>
|
||||
<Icon name={getRepositoryIcon()} size="xxl" />
|
||||
</Card.Figure>
|
||||
<Card.Heading>
|
||||
<Stack gap={2} direction="row" alignItems="center">
|
||||
{spec?.title && <Text variant="h3">{spec.title}</Text>}
|
||||
<StatusBadge enabled={Boolean(spec?.sync?.enabled)} state={status?.sync?.state} name={name} />
|
||||
</Stack>
|
||||
</Card.Heading>
|
||||
|
||||
<Card.Description>
|
||||
<Stack gap={2} direction="column">
|
||||
{spec?.description && <Text>{spec.description}</Text>}
|
||||
{status?.stats?.length && (
|
||||
<Stack gap={1} direction="row" wrap>
|
||||
{status.stats.map((stat, index) => (
|
||||
<LinkButton
|
||||
key={index}
|
||||
fill="outline"
|
||||
size="md"
|
||||
variant="secondary"
|
||||
href={getListURL(repository, stat)}
|
||||
>
|
||||
{stat.count} {stat.resource}
|
||||
</LinkButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Description>
|
||||
|
||||
<Card.Meta>
|
||||
<Stack gap={2} direction="row" wrap>
|
||||
{getRepositoryMeta()}
|
||||
</Stack>
|
||||
</Card.Meta>
|
||||
|
||||
<Card.Actions>
|
||||
<Stack gap={1} direction="row">
|
||||
<LinkButton icon="eye" href={`${PROVISIONING_URL}/${name}`} variant="primary" size="md">
|
||||
View
|
||||
</LinkButton>
|
||||
<SyncRepository repository={repository} />
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`} size="md">
|
||||
Settings
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<DeleteRepositoryButton name={name} />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function
|
||||
function getListURL(repo: Repository, stats: ResourceCount): string {
|
||||
if (stats.resource === 'playlists') {
|
||||
return '/playlists';
|
||||
}
|
||||
if (repo.spec?.sync.target === 'folder') {
|
||||
return `/dashboards/f/${repo.metadata?.name}`;
|
||||
}
|
||||
return '/dashboards';
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import {
|
||||
Card,
|
||||
EmptySearchResult,
|
||||
FilterInput,
|
||||
Icon,
|
||||
IconName,
|
||||
LinkButton,
|
||||
Stack,
|
||||
TextLink,
|
||||
Text,
|
||||
Alert,
|
||||
ConfirmModal,
|
||||
Tab,
|
||||
TabsBar,
|
||||
TagList,
|
||||
} from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DeleteRepositoryButton } from './DeleteRepositoryButton';
|
||||
import GettingStarted from './GettingStarted/GettingStarted';
|
||||
import GettingStartedPage from './GettingStarted/GettingStartedPage';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { SyncRepository } from './SyncRepository';
|
||||
import { Repository, ResourceCount, useDeletecollectionRepositoryMutation, useGetFrontendSettingsQuery } from './api';
|
||||
import { PROVISIONING_URL, CONNECT_URL } from './constants';
|
||||
import { useRepositoryList } from './hooks';
|
||||
import { checkSyncSettings } from './utils';
|
||||
|
||||
const appEvents = getAppEvents();
|
||||
|
||||
export default function RepositoryListPage() {
|
||||
const [items, isLoading] = useRepositoryList({ watch: true });
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
const [deleteAll, deleteAllResult] = useDeletecollectionRepositoryMutation();
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('repositories');
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteAllResult.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['All configured repositories deleted'],
|
||||
});
|
||||
}
|
||||
}, [deleteAllResult.isSuccess]);
|
||||
|
||||
if (!items?.length && !isLoading) {
|
||||
return <GettingStartedPage />;
|
||||
}
|
||||
|
||||
const onConfirmDelete = () => {
|
||||
deleteAll({});
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'repositories':
|
||||
return <RepositoryListPageContent items={items ?? []} />;
|
||||
case 'getting-started':
|
||||
return <GettingStarted />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navId="provisioning" subTitle="View and manage your configured repositories">
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
{settings.data?.legacyStorage && (
|
||||
<Alert
|
||||
title="Legacy storage detected"
|
||||
severity="error"
|
||||
buttonContent={<>Remove all configured repositories</>}
|
||||
onRemove={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
Configured repositories will not work while running legacy storage.
|
||||
</Alert>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={showDeleteModal}
|
||||
title="Delete all configured repositories"
|
||||
body="Are you sure you want to delete all configured repositories? This action cannot be undone."
|
||||
confirmText="Delete repositories"
|
||||
onConfirm={onConfirmDelete}
|
||||
onDismiss={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
<Stack direction="column" gap={2}>
|
||||
<TabsBar>
|
||||
<Tab
|
||||
label="Repositories"
|
||||
active={activeTab === 'repositories'}
|
||||
onChangeTab={() => setActiveTab('repositories')}
|
||||
/>
|
||||
<Tab
|
||||
label="Getting started"
|
||||
active={activeTab === 'getting-started'}
|
||||
onChangeTab={() => setActiveTab('getting-started')}
|
||||
/>
|
||||
</TabsBar>
|
||||
{renderTabContent()}
|
||||
</Stack>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryListPageContent({ items }: { items: Repository[] }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const filteredItems = items.filter((item) => item.metadata?.name?.includes(query));
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
const [instanceConnected] = checkSyncSettings(settings.data);
|
||||
|
||||
return (
|
||||
<Stack direction={'column'} gap={3}>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" value={query} onChange={setQuery} />
|
||||
{!instanceConnected && (
|
||||
<LinkButton href={CONNECT_URL} variant="primary" icon={'plus'}>
|
||||
Connect to repository
|
||||
</LinkButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction={'column'}>
|
||||
{!!filteredItems.length ? (
|
||||
filteredItems.map((item) => {
|
||||
const name = item.metadata?.name ?? '';
|
||||
|
||||
let icon: IconName = 'database'; // based on type
|
||||
let meta: ReactNode[] = [];
|
||||
switch (item.spec?.type) {
|
||||
case 'github':
|
||||
icon = 'github';
|
||||
const spec = item.spec.github;
|
||||
const url = item.spec.github?.url ?? '';
|
||||
let branch = url;
|
||||
if (spec?.branch) {
|
||||
branch += `/tree/` + spec?.branch;
|
||||
}
|
||||
meta.push(
|
||||
<TextLink key={'link'} external style={{ color: 'inherit' }} href={branch}>
|
||||
{branch}
|
||||
</TextLink>
|
||||
);
|
||||
|
||||
if (item.status?.webhook?.id) {
|
||||
const hook = url + `/settings/hooks/${item.status?.webhook?.id}`;
|
||||
meta.push(
|
||||
<TextLink key={'webhook'} style={{ color: 'inherit' }} href={hook}>
|
||||
Webhook <Icon name={'check'} />
|
||||
</TextLink>
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
meta.push(
|
||||
<Text element={'p'} key={'path'}>
|
||||
{item.spec.local?.path ?? ''}
|
||||
</Text>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={name}>
|
||||
<Card.Figure>
|
||||
<Icon name={icon} width={40} height={40} />
|
||||
</Card.Figure>
|
||||
<Card.Heading>
|
||||
<Stack>
|
||||
{item.spec?.title}{' '}
|
||||
<StatusBadge
|
||||
enabled={Boolean(item.spec?.sync?.enabled)}
|
||||
state={item.status?.sync?.state}
|
||||
name={name}
|
||||
/>
|
||||
<TagList tags={[item.spec?.sync?.target ?? '']} />
|
||||
</Stack>
|
||||
</Card.Heading>
|
||||
<Card.Description>
|
||||
{item.spec?.description}
|
||||
{item.status?.stats?.length && (
|
||||
<Stack>
|
||||
{item.status.stats.map((v, index) => (
|
||||
<LinkButton key={index} fill="outline" size="md" href={getListURL(item, v)}>
|
||||
{v.count} {v.resource}
|
||||
</LinkButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Card.Description>
|
||||
<Card.Meta>{meta}</Card.Meta>
|
||||
<Card.Actions>
|
||||
<LinkButton icon="eye" href={`${PROVISIONING_URL}/${name}`} variant="secondary">
|
||||
View
|
||||
</LinkButton>
|
||||
<SyncRepository repository={item} />
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`}>
|
||||
Settings
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<DeleteRepositoryButton name={name} />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<EmptySearchResult>No results matching your query</EmptySearchResult>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// This should return a URL in the UI that will show the selected values
|
||||
function getListURL(repo: Repository, stats: ResourceCount): string {
|
||||
if (stats.resource === 'playlists') {
|
||||
return '/playlists';
|
||||
}
|
||||
if (repo.spec?.sync.target === 'folder') {
|
||||
return `/dashboards/f/${repo.metadata?.name}`;
|
||||
}
|
||||
return '/dashboards';
|
||||
}
|
||||
@@ -1,48 +1,22 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { SelectableValue, urlUtil } from '@grafana/data';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
CellProps,
|
||||
Column,
|
||||
ConfirmModal,
|
||||
EmptyState,
|
||||
FilterInput,
|
||||
InteractiveTable,
|
||||
LinkButton,
|
||||
Spinner,
|
||||
Stack,
|
||||
Tab,
|
||||
TabContent,
|
||||
TabsBar,
|
||||
Text,
|
||||
TextLink,
|
||||
Modal,
|
||||
} from '@grafana/ui';
|
||||
import { Alert, EmptyState, Modal, Tab, TabContent, TabsBar, Text, TextLink } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
|
||||
import { isNotFoundError } from '../alerting/unified/api/util';
|
||||
|
||||
import { ExportToRepository } from './ExportToRepository';
|
||||
import { FilesView } from './FilesView';
|
||||
import { MigrateToRepository } from './MigrateToRepository';
|
||||
import { RepositoryActions } from './RepositoryActions';
|
||||
import { RepositoryOverview } from './RepositoryOverview';
|
||||
import { RepositoryResources } from './RepositoryResources';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { SyncRepository } from './SyncRepository';
|
||||
import {
|
||||
useGetRepositoryFilesQuery,
|
||||
Repository,
|
||||
useListRepositoryQuery,
|
||||
useDeleteRepositoryFilesWithPathMutation,
|
||||
useGetFrontendSettingsQuery,
|
||||
} from './api';
|
||||
import { FileDetails } from './api/types';
|
||||
import { useGetFrontendSettingsQuery, useListRepositoryQuery } from './api';
|
||||
import { PROVISIONING_URL } from './constants';
|
||||
import { getRemoteURL } from './utils/git';
|
||||
|
||||
enum TabSelection {
|
||||
Overview = 'overview',
|
||||
@@ -70,9 +44,9 @@ export default function RepositoryStatusPage() {
|
||||
const [queryParams] = useQueryParams();
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
const tab = queryParams['tab'] ?? TabSelection.Overview;
|
||||
const remoteURL = data ? getRemoteURL(data) : undefined;
|
||||
|
||||
const notFound = query.isError && isNotFoundError(query.error);
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
@@ -82,27 +56,12 @@ export default function RepositoryStatusPage() {
|
||||
}}
|
||||
actions={
|
||||
data && (
|
||||
<Stack>
|
||||
<StatusBadge enabled={Boolean(data.spec?.sync?.enabled)} state={data.status?.sync?.state} name={name} />
|
||||
{remoteURL && (
|
||||
<Button variant="secondary" icon="github" onClick={() => window.open(remoteURL, '_blank')}>
|
||||
Source Code
|
||||
</Button>
|
||||
)}
|
||||
<SyncRepository repository={data} />
|
||||
{settings.data?.legacyStorage ? (
|
||||
<Button variant="secondary" icon="cloud-upload" onClick={() => setShowMigrateModal(true)}>
|
||||
Migrate
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" icon="cloud-upload" onClick={() => setShowExportModal(true)}>
|
||||
Push
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`}>
|
||||
Settings
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
<RepositoryActions
|
||||
repository={data}
|
||||
showMigrateButton={settings.data?.legacyStorage}
|
||||
onExportClick={() => setShowExportModal(true)}
|
||||
onMigrateClick={() => setShowMigrateModal(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -158,99 +117,3 @@ export default function RepositoryStatusPage() {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
interface RepoProps {
|
||||
repo: Repository;
|
||||
}
|
||||
|
||||
type FileCell<T extends keyof FileDetails = keyof FileDetails> = CellProps<FileDetails, FileDetails[T]>;
|
||||
|
||||
function FilesView({ repo }: RepoProps) {
|
||||
const name = repo.metadata?.name ?? '';
|
||||
const query = useGetRepositoryFilesQuery({ name });
|
||||
const [deleteFile, deleteFileStatus] = useDeleteRepositoryFilesWithPathMutation();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [pathToDelete, setPathToDelete] = useState<string>();
|
||||
const data = [...(query.data?.items ?? [])].filter((file) =>
|
||||
file.path.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const columns: Array<Column<FileDetails>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'path',
|
||||
header: 'Path',
|
||||
sortType: 'string',
|
||||
cell: ({ row: { original } }: FileCell<'path'>) => {
|
||||
const { path } = original;
|
||||
return <a href={`${PROVISIONING_URL}/${name}/file/${path}`}>{path}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
header: 'Size (KB)',
|
||||
cell: ({ row: { original } }: FileCell<'size'>) => {
|
||||
const { size } = original;
|
||||
return (parseInt(size, 10) / 1024).toFixed(2);
|
||||
},
|
||||
sortType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'hash',
|
||||
header: 'Hash',
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row: { original } }: FileCell<'path'>) => {
|
||||
const { path } = original;
|
||||
return (
|
||||
<Stack>
|
||||
{(path.endsWith('.json') || path.endsWith('.yaml') || path.endsWith('.yml')) && (
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/file/${path}`}>View</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/history/${path}`}>History</LinkButton>
|
||||
<Button variant="destructive" onClick={() => setPathToDelete(path)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[name]
|
||||
);
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<Stack justifyContent={'center'} alignItems={'center'}>
|
||||
<Spinner />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack grow={1} direction={'column'} gap={2}>
|
||||
<ConfirmModal
|
||||
isOpen={Boolean(pathToDelete?.length) || deleteFileStatus.isLoading}
|
||||
title="Delete file in repository?"
|
||||
body={deleteFileStatus.isLoading ? 'Deleting file...' : pathToDelete}
|
||||
confirmText="Delete"
|
||||
icon={deleteFileStatus.isLoading ? `spinner` : `exclamation-triangle`}
|
||||
onConfirm={() => {
|
||||
deleteFile({
|
||||
name: name,
|
||||
path: pathToDelete!,
|
||||
message: `Deleted from repo test UI`,
|
||||
});
|
||||
setPathToDelete('');
|
||||
}}
|
||||
onDismiss={() => setPathToDelete('')}
|
||||
/>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" autoFocus={true} value={searchQuery} onChange={setSearchQuery} />
|
||||
</Stack>
|
||||
<InteractiveTable columns={columns} data={data} pageSize={25} getRowId={(f: FileDetails) => String(f.path)} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -29,9 +29,10 @@ const modeOptions = [
|
||||
|
||||
type Props = {
|
||||
targetSelectable?: boolean;
|
||||
generateName?: boolean;
|
||||
};
|
||||
|
||||
export function ConnectionStep({ targetSelectable = true }: Props) {
|
||||
export function ConnectionStep({ targetSelectable = true, generateName = false }: Props) {
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@@ -58,7 +59,12 @@ export function ConnectionStep({ targetSelectable = true }: Props) {
|
||||
if (folderConnected) {
|
||||
setValue('repository.sync.target', folderConnected ? 'folder' : 'instance');
|
||||
}
|
||||
}, [folderConnected, setValue]);
|
||||
if (generateName) {
|
||||
// Generate a unique title with timestamp to ensure uniqueness
|
||||
const timestamp = Date.now();
|
||||
setValue('repository.title', `instance-${timestamp}`);
|
||||
}
|
||||
}, [folderConnected, generateName, setValue]);
|
||||
|
||||
return (
|
||||
<FieldSet label="1. Set up your repository connection details">
|
||||
@@ -88,17 +94,20 @@ export function ConnectionStep({ targetSelectable = true }: Props) {
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Display name"
|
||||
description="Add a clear name for this repository connection"
|
||||
error={errors.repository?.title?.message}
|
||||
invalid={!!errors.repository?.title}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.title', { required: 'This field is required.' })}
|
||||
placeholder="My repository connection"
|
||||
/>
|
||||
</Field>
|
||||
{/* Only show title field if not instance sync */}
|
||||
{!generateName && (
|
||||
<Field
|
||||
label="Display name"
|
||||
description="Add a clear name for this repository connection"
|
||||
error={errors.repository?.title?.message}
|
||||
invalid={!!errors.repository?.title}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.title', { required: 'This field is required.' })}
|
||||
placeholder="My repository connection"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Stack direction="column" gap={2}>
|
||||
{folderConnected && (
|
||||
|
||||
@@ -115,7 +115,9 @@ export function ProvisioningWizard({ requiresMigration }: Props) {
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
{activeStep === 'connection' && <ConnectionStep targetSelectable={!requiresMigration} />}
|
||||
{activeStep === 'connection' && (
|
||||
<ConnectionStep targetSelectable={!requiresMigration} generateName={requiresMigration} />
|
||||
)}
|
||||
{activeStep === 'repository' && <RepositoryStep onStatusChange={handleStatusChange} />}
|
||||
{activeStep === 'migrate' && requiresMigration && <MigrateStep onStatusChange={handleStatusChange} />}
|
||||
{activeStep === 'pull' && !requiresMigration && <PullStep onStatusChange={handleStatusChange} />}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function getProvisioningRoutes(): RouteDescriptor[] {
|
||||
{
|
||||
path: PROVISIONING_URL,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "RepositoryListPage"*/ 'app/features/provisioning/RepositoryListPage')
|
||||
() => import(/* webpackChunkName: "RepositoryListPage"*/ 'app/features/provisioning/HomePage')
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user