POC/Provisioning: Enable sync (#101131)
This commit is contained in:
1 parent
6e7fb03c3a
commit
54d84126ab
13 files changed
+198
-86
No files matched your search
+12
-6
@@ -5766,8 +5766,7 @@ exports[`better eslint`] = {
|
||||
"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. 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"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/MigrateToRepository.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
@@ -5785,7 +5784,8 @@ exports[`better eslint`] = {
|
||||
[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"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
|
||||
],
|
||||
"public/app/features/provisioning/RecentJobs.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
@@ -5905,10 +5905,12 @@ exports[`better eslint`] = {
|
||||
[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 in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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 />", "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/Wizard/ProvisioningWizard.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
@@ -5927,7 +5929,11 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "11"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "12"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "14"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "15"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "16"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "17"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/Stepper.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
|
||||
@@ -55,7 +55,7 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat
|
||||
generateDashboardPreviews: true,
|
||||
workflows: ['write', 'branch'],
|
||||
sync: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
target: 'instance',
|
||||
intervalSeconds: 60,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Box, ControlledCollapse, Stack, Text, TextLink } from '@grafana/ui';
|
||||
|
||||
import ProgressBar from './ProgressBar';
|
||||
import { useGetRepositoryQuery, useListJobQuery } from './api';
|
||||
import { getRemoteURL } from './utils/git';
|
||||
|
||||
export function JobStatus({ name }: { name: string }) {
|
||||
const jobQuery = useListJobQuery({ watch: true, fieldSelector: `metadata.name=${name}` });
|
||||
@@ -18,8 +19,8 @@ export function JobStatus({ name }: { name: string }) {
|
||||
<Stack direction={'column'} gap={2}>
|
||||
{job.status && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text element="p">
|
||||
{job.status.message} // {job.status.state}
|
||||
<Text element="p" weight="medium">
|
||||
{job.status.message ?? ''}
|
||||
</Text>
|
||||
<ProgressBar progress={job.status.progress} />
|
||||
|
||||
@@ -46,10 +47,15 @@ function RepositoryLink({ name }: RepositoryLinkProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const href = getRemoteURL(repo);
|
||||
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Stack direction={'column'}>
|
||||
<Text>Your dashboards and folders are now in your repository.</Text>
|
||||
<TextLink external href={repo.spec?.github?.url}>
|
||||
<TextLink external href={href}>
|
||||
View repository
|
||||
</TextLink>
|
||||
</Stack>
|
||||
|
||||
@@ -15,6 +15,7 @@ export function MigrateToRepository({ repo }: Props) {
|
||||
|
||||
const { register, formState, handleSubmit } = useForm<MigrateJobOptions>({
|
||||
defaultValues: {
|
||||
identifier: true, // Keep URLs the same
|
||||
history: true,
|
||||
prefix: '',
|
||||
},
|
||||
|
||||
@@ -4,9 +4,9 @@ import { EmptyState, LinkButton, Alert, Stack, Text } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { SetupWarnings } from './SetupWarnings';
|
||||
import { MIGRATE_URL } from './constants';
|
||||
import { NEW_URL, MIGRATE_URL } from './constants';
|
||||
|
||||
export default function OnboardingPage() {
|
||||
export default function OnboardingPage({ legacyStorage }: { legacyStorage?: boolean }) {
|
||||
return (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
@@ -14,28 +14,42 @@ export default function OnboardingPage() {
|
||||
>
|
||||
<Page.Contents>
|
||||
<SetupWarnings />
|
||||
<Alert severity="info" title="Setting up this connection could cause a temporary outage">
|
||||
When you connect your whole instance, dashboards will be unavailable while running the migration. We recommend
|
||||
warning your users before starting the process.
|
||||
</Alert>
|
||||
<EmptyState
|
||||
variant="call-to-action"
|
||||
message="Set up your provisioning connection!"
|
||||
image={<SVG src="public/img/provisioning-empty.svg" width={300} />}
|
||||
button={
|
||||
<LinkButton size="lg" icon="plus" href={MIGRATE_URL}>
|
||||
Connect Grafana to repository
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
<Stack direction="column" alignItems="center">
|
||||
<Text>Store and provision your Grafana resources externally by connecting to a repository.</Text>
|
||||
<Text>We currently support GitHub and local storage.</Text>
|
||||
<LinkButton fill="text" href="#" icon="external-link-alt">
|
||||
Learn more
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</EmptyState>
|
||||
{legacyStorage ? (
|
||||
<>
|
||||
<Alert severity="info" title="Setting up this connection could cause a temporary outage">
|
||||
When you connect your whole instance, dashboards will be unavailable while running the migration. We
|
||||
recommend warning your users before starting the process.
|
||||
</Alert>
|
||||
<EmptyState
|
||||
variant="call-to-action"
|
||||
message="Set up your provisioning connection!"
|
||||
image={<SVG src="public/img/provisioning-empty.svg" width={300} />}
|
||||
button={
|
||||
<LinkButton size="lg" icon="plus" href={MIGRATE_URL}>
|
||||
Connect Grafana to repository
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
<Stack direction="column" alignItems="center">
|
||||
<Text>Store and provision your Grafana resources externally by connecting to a repository.</Text>
|
||||
<Text>We currently support GitHub and local storage.</Text>
|
||||
<LinkButton fill="text" href="#" icon="external-link-alt">
|
||||
Learn more
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</EmptyState>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
variant="call-to-action"
|
||||
message="You haven't created any repository configs yet"
|
||||
button={
|
||||
<LinkButton icon="plus" href={NEW_URL} size="lg">
|
||||
Create repository config
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function RepositoryListPage() {
|
||||
}, [deleteAllResult.isSuccess]);
|
||||
|
||||
if (!items?.length && !isLoading) {
|
||||
return <OnboardingPage />;
|
||||
return <OnboardingPage legacyStorage={settings.data?.legacyStorage} />;
|
||||
}
|
||||
|
||||
const onConfirmDelete = () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from './api';
|
||||
import { FileDetails } from './api/types';
|
||||
import { PROVISIONING_URL } from './constants';
|
||||
import { getRemoteURL } from './utils/git';
|
||||
|
||||
enum TabSelection {
|
||||
Overview = 'overview',
|
||||
@@ -253,15 +254,3 @@ function FilesView({ repo }: RepoProps) {
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getRemoteURL(repo: Repository) {
|
||||
if (repo.spec?.type === 'github') {
|
||||
const spec = repo.spec.github;
|
||||
let url = spec?.url || '';
|
||||
if (spec?.branch) {
|
||||
url += `/tree/${spec.branch}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -2,11 +2,12 @@ import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
import { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Button, FieldSet, Stack, Text, Switch, Field, Alert } from '@grafana/ui';
|
||||
import { Alert, Button, FieldSet, Stack, Text, Switch, Field } from '@grafana/ui';
|
||||
|
||||
import { JobStatus } from '../JobStatus';
|
||||
import { useCreateRepositoryMigrateMutation, useGetRepositoryQuery } from '../api';
|
||||
|
||||
import { RequestErrorAlert } from './RequestErrorAlert';
|
||||
import { WizardFormData } from './types';
|
||||
|
||||
export interface MigrateStepProps {
|
||||
@@ -58,6 +59,13 @@ export function MigrateStep({ onMigrationStatusChange }: MigrateStepProps) {
|
||||
updates will be automatically saved to the repository.
|
||||
</Text>
|
||||
|
||||
{!repositoryName && (
|
||||
<Alert severity="error" title="Repository name required">
|
||||
Repository name is required to migrate dashboards. Please complete the repository configuration step first.
|
||||
</Alert>
|
||||
)}
|
||||
<RequestErrorAlert request={migrateQuery} />
|
||||
|
||||
<Alert severity="info" title="Note">
|
||||
Dashboards will be unavailable while running this process.
|
||||
</Alert>
|
||||
|
||||
@@ -35,11 +35,12 @@ export function ProvisioningWizard() {
|
||||
const navigate = useNavigate();
|
||||
const [activeStep, setActiveStep] = useState<WizardStep>('connection');
|
||||
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
|
||||
const [migrationSuccess, setMigrationSuccess] = useState(false);
|
||||
const [stepSuccess, setStepSuccess] = useState(false);
|
||||
const [submitData, request] = useCreateOrUpdateRepository();
|
||||
const methods = useForm<WizardFormData>({
|
||||
defaultValues: {
|
||||
repository: getDefaultValues(),
|
||||
|
||||
migrate: {
|
||||
history: true,
|
||||
identifier: false,
|
||||
@@ -51,19 +52,20 @@ export function ProvisioningWizard() {
|
||||
|
||||
useEffect(() => {
|
||||
if (request.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository settings saved'],
|
||||
});
|
||||
|
||||
if (request.data?.metadata?.name) {
|
||||
methods.setValue('repositoryName', request.data.metadata.name);
|
||||
}
|
||||
if (activeStep === 'repository') {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository settings saved'],
|
||||
});
|
||||
|
||||
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
setCompletedSteps([...completedSteps, activeStep]);
|
||||
setActiveStep(steps[currentStepIndex + 1].id);
|
||||
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
setCompletedSteps((prev) => [...prev, activeStep]);
|
||||
setActiveStep(steps[currentStepIndex + 1].id);
|
||||
}
|
||||
}
|
||||
} else if (request.isError) {
|
||||
appEvents.publish({
|
||||
@@ -71,7 +73,7 @@ export function ProvisioningWizard() {
|
||||
payload: ['Failed to save repository settings', request.error],
|
||||
});
|
||||
}
|
||||
}, [request.isSuccess, request.isError, request.data, request.error, activeStep, completedSteps, methods]);
|
||||
}, [request.isSuccess, request.isError, request.data, request.error, activeStep, methods]);
|
||||
|
||||
const handleNext = async () => {
|
||||
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
|
||||
@@ -90,9 +92,12 @@ export function ProvisioningWizard() {
|
||||
}
|
||||
}
|
||||
|
||||
setCompletedSteps([...completedSteps, activeStep]);
|
||||
if (activeStep === 'migrate') {
|
||||
setCompletedSteps((prev) => [...prev, activeStep]);
|
||||
}
|
||||
setActiveStep(steps[currentStepIndex + 1].id);
|
||||
} else if (activeStep === 'migrate' && migrationSuccess) {
|
||||
setStepSuccess(false);
|
||||
} else if (activeStep === 'migrate') {
|
||||
navigate('/dashboards');
|
||||
}
|
||||
};
|
||||
@@ -101,6 +106,9 @@ export function ProvisioningWizard() {
|
||||
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
|
||||
if (currentStepIndex > 0) {
|
||||
setActiveStep(steps[currentStepIndex - 1].id);
|
||||
setStepSuccess(false);
|
||||
// Remove the last completed step when going back
|
||||
setCompletedSteps((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,19 +125,18 @@ export function ProvisioningWizard() {
|
||||
<Stepper
|
||||
steps={steps}
|
||||
activeStep={activeStep}
|
||||
onStepChange={() => {}}
|
||||
visitedSteps={completedSteps}
|
||||
validationResults={{
|
||||
connection: { valid: true },
|
||||
repository: { valid: true },
|
||||
migrate: { valid: true },
|
||||
}}
|
||||
getNextUrl={() => ''}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
{activeStep === 'connection' && <ConnectionStep />}
|
||||
{activeStep === 'repository' && <RepositoryStep />}
|
||||
{activeStep === 'migrate' && <MigrateStep onMigrationStatusChange={setMigrationSuccess} />}
|
||||
{activeStep === 'repository' && <RepositoryStep request={request} />}
|
||||
{activeStep === 'migrate' && <MigrateStep onMigrationStatusChange={setStepSuccess} />}
|
||||
</div>
|
||||
|
||||
<Stack gap={2} justifyContent="flex-end">
|
||||
@@ -140,7 +147,11 @@ export function ProvisioningWizard() {
|
||||
)}
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={(activeStep === 'migrate' && !migrationSuccess) || request.isLoading}
|
||||
disabled={
|
||||
request.isLoading ||
|
||||
(activeStep === 'repository' && (request.isLoading || request.isError)) ||
|
||||
(activeStep === 'migrate' && (!stepSuccess || request.isLoading))
|
||||
}
|
||||
icon={request.isLoading ? 'spinner' : undefined}
|
||||
>
|
||||
{getButtonText()}
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Field, FieldSet, Input, MultiCombobox, SecretInput, Switch } from '@grafana/ui';
|
||||
import { Field, FieldSet, Input, MultiCombobox, SecretInput, Stack, Switch, ControlledCollapse } from '@grafana/ui';
|
||||
|
||||
import { getWorkflowOptions } from '../ConfigForm';
|
||||
import { TokenPermissionsInfo } from '../TokenPermissionsInfo';
|
||||
|
||||
import { RequestErrorAlert } from './RequestErrorAlert';
|
||||
import { WizardFormData } from './types';
|
||||
|
||||
export function RepositoryStep() {
|
||||
interface Props {
|
||||
request: {
|
||||
isError: boolean;
|
||||
error?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
const AdvancedSettingsFields = () => {
|
||||
const { register } = useFormContext<WizardFormData>();
|
||||
|
||||
return (
|
||||
<ControlledCollapse label="Advanced settings" isOpen={true}>
|
||||
<Field label={'Enable sync'}>
|
||||
<Switch {...register('repository.sync.enabled')} />
|
||||
</Field>
|
||||
<Field label={'Sync interval (seconds)'}>
|
||||
<Input
|
||||
{...register('repository.sync.intervalSeconds', { valueAsNumber: true })}
|
||||
type={'number'}
|
||||
placeholder={'60'}
|
||||
/>
|
||||
</Field>
|
||||
</ControlledCollapse>
|
||||
);
|
||||
};
|
||||
|
||||
export function RepositoryStep({ request }: Props) {
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@@ -50,7 +77,8 @@ export function RepositoryStep() {
|
||||
if (type === 'github') {
|
||||
return (
|
||||
<FieldSet label="2. Configure repository">
|
||||
<div>
|
||||
<Stack direction="column" gap={1}>
|
||||
<RequestErrorAlert request={request} />
|
||||
<TokenPermissionsInfo />
|
||||
|
||||
<Field
|
||||
@@ -108,7 +136,8 @@ export function RepositoryStep() {
|
||||
</Field>
|
||||
|
||||
<WorkflowsField />
|
||||
</div>
|
||||
<AdvancedSettingsFields />
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
@@ -116,14 +145,19 @@ export function RepositoryStep() {
|
||||
if (type === 'local') {
|
||||
return (
|
||||
<FieldSet label="2. Configure repository">
|
||||
<Field label={'Local path'} error={errors.repository?.path?.message} invalid={!!errors.repository?.path}>
|
||||
<Input
|
||||
{...register('repository.path', { required: 'This field is required.' })}
|
||||
placeholder={'/path/to/repo'}
|
||||
/>
|
||||
</Field>
|
||||
<Stack direction="column" gap={2}>
|
||||
<RequestErrorAlert request={request} />
|
||||
|
||||
<WorkflowsField />
|
||||
<Field label={'Local path'} error={errors.repository?.path?.message} invalid={!!errors.repository?.path}>
|
||||
<Input
|
||||
{...register('repository.path', { required: 'This field is required.' })}
|
||||
placeholder={'/path/to/repo'}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<WorkflowsField />
|
||||
<AdvancedSettingsFields />
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
|
||||
interface RequestErrorAlertProps {
|
||||
request: {
|
||||
isError: boolean;
|
||||
error?: unknown;
|
||||
endpointName?: string;
|
||||
};
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function getDefaultTitle(endpointName?: string): string {
|
||||
switch (endpointName) {
|
||||
case 'createRepositoryMigrate':
|
||||
return 'Failed to migrate dashboards';
|
||||
case 'createOrUpdateRepository':
|
||||
return 'Failed to save repository settings';
|
||||
default:
|
||||
return 'Operation failed';
|
||||
}
|
||||
}
|
||||
|
||||
export function RequestErrorAlert({ request, title }: RequestErrorAlertProps) {
|
||||
if (!request.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorTitle = title || getDefaultTitle(request.endpointName);
|
||||
|
||||
return (
|
||||
<Alert severity="error" title={errorTitle}>
|
||||
{getMessageFromError(request.error)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, Icon } from '@grafana/ui';
|
||||
@@ -13,19 +12,15 @@ export interface Step<T> {
|
||||
|
||||
export interface Props<T extends string | number> {
|
||||
activeStep?: T;
|
||||
onStepChange: (step: T, event: MouseEvent<HTMLAnchorElement>) => void;
|
||||
reportId?: string;
|
||||
visitedSteps?: T[];
|
||||
steps: Array<Step<T>>;
|
||||
validationResults: Record<T, ValidationResult>;
|
||||
getNextUrl: (step: T) => string;
|
||||
}
|
||||
|
||||
export function Stepper<T extends string | number>({
|
||||
validationResults,
|
||||
onStepChange,
|
||||
visitedSteps = [],
|
||||
getNextUrl,
|
||||
steps,
|
||||
activeStep = steps[0]?.id,
|
||||
}: Props<T>) {
|
||||
@@ -37,9 +32,9 @@ export function Stepper<T extends string | number>({
|
||||
{steps.map((step) => {
|
||||
const isLast = step.id === lastStep.id;
|
||||
const isActive = step.id === activeStep;
|
||||
const isVisited = visitedSteps.includes(step.id) || activeStep === lastStep.id;
|
||||
const hasMissingFields = !isLast && !validationResults[step.id].valid;
|
||||
const showIndicator = !isActive && !isLast && isVisited;
|
||||
const isVisited = visitedSteps.includes(step.id);
|
||||
const hasMissingFields = !validationResults[step.id].valid;
|
||||
const showIndicator = !isActive && isVisited;
|
||||
const successField = showIndicator && !hasMissingFields;
|
||||
const warnField = showIndicator && hasMissingFields;
|
||||
const itemStyles = cx(styles.item, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RepositorySpec } from '../api';
|
||||
import { Repository, RepositorySpec } from '../api';
|
||||
|
||||
export function createPRLink(spec?: RepositorySpec, dashboardName?: string, ref?: string, comment?: string) {
|
||||
if (!spec || spec.type !== 'github' || !ref) {
|
||||
@@ -21,3 +21,15 @@ export function validateBranchName(branchName?: string) {
|
||||
|
||||
return branchName && branchNameRegex.test(branchName!);
|
||||
}
|
||||
|
||||
export function getRemoteURL(repo: Repository) {
|
||||
if (repo.spec?.type === 'github') {
|
||||
const spec = repo.spec.github;
|
||||
let url = spec?.url || '';
|
||||
if (spec?.branch) {
|
||||
url += `/tree/${spec.branch}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Reference in new issue
Block a user