mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(ui): add provider wizard modal with step wrappers
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ProviderWizardModal } from "@/components/providers/wizard";
|
||||
import { Button } from "@/components/shadcn";
|
||||
|
||||
import { AddIcon } from "../icons";
|
||||
|
||||
export const AddProviderButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Button asChild>
|
||||
<Link href="/providers/connect-account">
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
Add Cloud Provider
|
||||
<AddIcon size={20} />
|
||||
</Link>
|
||||
</Button>
|
||||
</Button>
|
||||
<ProviderWizardModal open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,11 @@ export function RadioCard({
|
||||
className={cn(
|
||||
"flex min-h-[72px] w-full items-center gap-4 rounded-lg border px-3 py-2.5 text-left transition-colors",
|
||||
disabled
|
||||
? "cursor-not-allowed border-border-neutral-primary bg-bg-neutral-tertiary"
|
||||
: "hover:border-primary cursor-pointer border-border-neutral-primary bg-bg-neutral-tertiary",
|
||||
? "border-border-neutral-primary bg-bg-neutral-tertiary cursor-not-allowed"
|
||||
: "hover:border-primary border-border-neutral-primary bg-bg-neutral-tertiary cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<div className="size-[18px] shrink-0 rounded-full border border-border-neutral-primary bg-bg-input-primary shadow-xs" />
|
||||
<div className="border-border-neutral-primary bg-bg-input-primary size-[18px] shrink-0 rounded-full border shadow-xs" />
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<Icon
|
||||
|
||||
@@ -102,7 +102,7 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
|
||||
name="providerType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="flex h-[calc(100vh-200px)] flex-col px-4">
|
||||
<div className="flex flex-col px-4">
|
||||
<div className="relative z-10 shrink-0 pb-4">
|
||||
<SearchInput
|
||||
aria-label="Search providers"
|
||||
@@ -113,19 +113,11 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="minimal-scrollbar relative flex-1 overflow-y-auto pr-3">
|
||||
<div className="relative">
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label="Select a provider"
|
||||
className="flex flex-col gap-3"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
paddingTop: "24px",
|
||||
marginTop: "-24px",
|
||||
}}
|
||||
>
|
||||
{filteredProviders.length > 0 ? (
|
||||
filteredProviders.map((provider) => {
|
||||
@@ -140,19 +132,26 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
|
||||
aria-selected={isSelected}
|
||||
onClick={() => field.onChange(provider.value)}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-3 rounded-lg border p-4 text-left transition-all",
|
||||
"hover:border-button-primary",
|
||||
"focus-visible:border-button-primary focus-visible:ring-button-primary focus:outline-none focus-visible:ring-1",
|
||||
"flex min-h-[72px] w-full items-center gap-4 rounded-lg border px-3 py-2.5 text-left transition-colors",
|
||||
"focus-visible:border-primary focus-visible:outline-none",
|
||||
isSelected
|
||||
? "border-button-primary bg-bg-neutral-tertiary"
|
||||
: "border-border-neutral-secondary bg-bg-neutral-secondary",
|
||||
? "border-primary bg-bg-neutral-tertiary"
|
||||
: "border-border-neutral-primary bg-bg-neutral-tertiary hover:border-primary",
|
||||
isInvalid && "border-bg-fail",
|
||||
)}
|
||||
>
|
||||
<BadgeComponent size={26} />
|
||||
<span className="text-text-neutral-primary text-sm font-medium">
|
||||
{provider.label}
|
||||
</span>
|
||||
<div className="border-border-neutral-primary bg-bg-input-primary flex size-[18px] shrink-0 items-center justify-center rounded-full border shadow-xs">
|
||||
{isSelected && (
|
||||
<div className="bg-primary size-2.5 rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<BadgeComponent size={26} />
|
||||
<span className="text-foreground text-sm leading-6">
|
||||
{provider.label}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { Row } from "@tanstack/react-table";
|
||||
import { Pencil, PlugZap, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { checkConnectionProvider } from "@/actions/providers/providers";
|
||||
import { VerticalDotsIcon } from "@/components/icons";
|
||||
import { ProviderWizardModal } from "@/components/providers/wizard";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import {
|
||||
ActionDropdown,
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
ActionDropdownItem,
|
||||
} from "@/components/shadcn/dropdown";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
import { EditForm } from "../forms";
|
||||
import { DeleteForm } from "../forms/delete-form";
|
||||
@@ -25,13 +27,15 @@ interface DataTableRowActionsProps<ProviderProps> {
|
||||
export function DataTableRowActions<ProviderProps>({
|
||||
row,
|
||||
}: DataTableRowActionsProps<ProviderProps>) {
|
||||
const router = useRouter();
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const providerId = (row.original as { id: string }).id;
|
||||
const providerType = (row.original as any).attributes?.provider;
|
||||
const providerAlias = (row.original as any).attributes?.alias;
|
||||
const providerType = (row.original as any).attributes
|
||||
?.provider as ProviderType;
|
||||
const providerUid = (row.original as any).attributes?.uid || "";
|
||||
const providerAlias = (row.original as any).attributes?.alias || null;
|
||||
const providerSecretId =
|
||||
(row.original as any).relationships?.secret?.data?.id || null;
|
||||
|
||||
@@ -66,6 +70,20 @@ export function DataTableRowActions<ProviderProps>({
|
||||
>
|
||||
<DeleteForm providerId={providerId} setIsOpen={setIsDeleteOpen} />
|
||||
</Modal>
|
||||
<ProviderWizardModal
|
||||
open={isWizardOpen}
|
||||
onOpenChange={setIsWizardOpen}
|
||||
initialData={{
|
||||
providerId,
|
||||
providerType,
|
||||
providerUid,
|
||||
providerAlias,
|
||||
secretId: providerSecretId,
|
||||
mode: providerSecretId
|
||||
? PROVIDER_WIZARD_MODE.UPDATE
|
||||
: PROVIDER_WIZARD_MODE.ADD,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<ActionDropdown
|
||||
@@ -78,11 +96,7 @@ export function DataTableRowActions<ProviderProps>({
|
||||
<ActionDropdownItem
|
||||
icon={<Pencil />}
|
||||
label={hasSecret ? "Update Credentials" : "Add Credentials"}
|
||||
onSelect={() =>
|
||||
router.push(
|
||||
`/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`,
|
||||
)
|
||||
}
|
||||
onSelect={() => setIsWizardOpen(true)}
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<PlugZap />}
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
"use client";
|
||||
|
||||
import { ExternalLink, Info } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { OrgAccountSelection } from "@/components/providers/organizations/org-account-selection";
|
||||
import { OrgConnectionTest } from "@/components/providers/organizations/org-connection-test";
|
||||
import { OrgDiscoveryLoader } from "@/components/providers/organizations/org-discovery-loader";
|
||||
import { OrgLaunchScan } from "@/components/providers/organizations/org-launch-scan";
|
||||
import { OrgSetupForm } from "@/components/providers/organizations/org-setup-form";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import { DialogHeader, DialogTitle } from "@/components/shadcn/dialog";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { getProviderHelpText } from "@/lib";
|
||||
import { useOrgSetupStore } from "@/store/organizations/store";
|
||||
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||
import { ORG_WIZARD_STEP, OrgWizardStep } from "@/types/organizations";
|
||||
import {
|
||||
PROVIDER_WIZARD_MODE,
|
||||
PROVIDER_WIZARD_STEP,
|
||||
ProviderWizardMode,
|
||||
ProviderWizardStep,
|
||||
} from "@/types/provider-wizard";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
import { ConnectStep } from "./steps/connect-step";
|
||||
import { CredentialsStep } from "./steps/credentials-step";
|
||||
import {
|
||||
WIZARD_FOOTER_ACTION_TYPE,
|
||||
WizardFooterConfig,
|
||||
} from "./steps/footer-controls";
|
||||
import { LaunchStep } from "./steps/launch-step";
|
||||
import { TestConnectionStep } from "./steps/test-connection-step";
|
||||
import { WizardStepper } from "./wizard-stepper";
|
||||
|
||||
const WIZARD_VARIANT = {
|
||||
PROVIDER: "provider",
|
||||
ORGANIZATIONS: "organizations",
|
||||
} as const;
|
||||
|
||||
type WizardVariant = (typeof WIZARD_VARIANT)[keyof typeof WIZARD_VARIANT];
|
||||
|
||||
const VALIDATE_PHASE = {
|
||||
DISCOVERY: "discovery",
|
||||
SELECTION: "selection",
|
||||
TESTING: "testing",
|
||||
} as const;
|
||||
|
||||
type ValidatePhase = (typeof VALIDATE_PHASE)[keyof typeof VALIDATE_PHASE];
|
||||
|
||||
const EMPTY_FOOTER_CONFIG: WizardFooterConfig = {
|
||||
showBack: false,
|
||||
backLabel: "Back",
|
||||
showAction: false,
|
||||
actionLabel: "Next",
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
};
|
||||
|
||||
export interface ProviderWizardInitialData {
|
||||
providerId: string;
|
||||
providerType: ProviderType;
|
||||
providerUid: string;
|
||||
providerAlias: string | null;
|
||||
secretId?: string | null;
|
||||
via?: string | null;
|
||||
mode?: ProviderWizardMode;
|
||||
}
|
||||
|
||||
interface ProviderWizardModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialData?: ProviderWizardInitialData;
|
||||
}
|
||||
|
||||
export function ProviderWizardModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialData,
|
||||
}: ProviderWizardModalProps) {
|
||||
const router = useRouter();
|
||||
const [wizardVariant, setWizardVariant] = useState<WizardVariant>(
|
||||
WIZARD_VARIANT.PROVIDER,
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState<ProviderWizardStep>(
|
||||
PROVIDER_WIZARD_STEP.CONNECT,
|
||||
);
|
||||
const [orgCurrentStep, setOrgCurrentStep] = useState<OrgWizardStep>(
|
||||
ORG_WIZARD_STEP.SETUP,
|
||||
);
|
||||
const [validatePhase, setValidatePhase] = useState<ValidatePhase>(
|
||||
VALIDATE_PHASE.DISCOVERY,
|
||||
);
|
||||
const [footerConfig, setFooterConfig] =
|
||||
useState<WizardFooterConfig>(EMPTY_FOOTER_CONFIG);
|
||||
const [providerTypeHint, setProviderTypeHint] = useState<ProviderType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const {
|
||||
reset: resetProviderWizard,
|
||||
setProvider,
|
||||
setVia,
|
||||
setSecretId,
|
||||
setMode,
|
||||
mode,
|
||||
providerType,
|
||||
} = useProviderWizardStore();
|
||||
const { connectionResults, reset: resetOrgWizard } = useOrgSetupStore();
|
||||
|
||||
const hasConnectionErrors = Object.values(connectionResults).some(
|
||||
(status) => status === "error",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialData) {
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setProvider({
|
||||
id: initialData.providerId,
|
||||
type: initialData.providerType,
|
||||
uid: initialData.providerUid,
|
||||
alias: initialData.providerAlias,
|
||||
});
|
||||
setVia(initialData.via || null);
|
||||
setSecretId(initialData.secretId || null);
|
||||
setMode(
|
||||
initialData.mode ||
|
||||
(initialData.secretId
|
||||
? PROVIDER_WIZARD_MODE.UPDATE
|
||||
: PROVIDER_WIZARD_MODE.ADD),
|
||||
);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(initialData.providerType);
|
||||
return;
|
||||
}
|
||||
|
||||
resetProviderWizard();
|
||||
resetOrgWizard();
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
}, [
|
||||
initialData,
|
||||
open,
|
||||
resetOrgWizard,
|
||||
resetProviderWizard,
|
||||
setMode,
|
||||
setProvider,
|
||||
setSecretId,
|
||||
setVia,
|
||||
]);
|
||||
|
||||
const handleClose = () => {
|
||||
resetProviderWizard();
|
||||
resetOrgWizard();
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen) {
|
||||
onOpenChange(true);
|
||||
return;
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleTestSuccess = () => {
|
||||
if (mode === PROVIDER_WIZARD_MODE.UPDATE) {
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
||||
};
|
||||
|
||||
const openOrganizationsFlow = () => {
|
||||
resetOrgWizard();
|
||||
setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
};
|
||||
|
||||
const backToProviderFlow = () => {
|
||||
resetOrgWizard();
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
};
|
||||
|
||||
const isProviderFlow = wizardVariant === WIZARD_VARIANT.PROVIDER;
|
||||
const docsLink = getProviderHelpText(
|
||||
isProviderFlow ? (providerTypeHint ?? providerType ?? "") : "aws",
|
||||
).link;
|
||||
const resolvedFooterConfig: WizardFooterConfig =
|
||||
isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH
|
||||
? {
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
onBack: () => setCurrentStep(PROVIDER_WIZARD_STEP.TEST),
|
||||
showAction: true,
|
||||
actionLabel: "Go to scans",
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
onAction: () => {
|
||||
handleClose();
|
||||
router.push("/scans");
|
||||
},
|
||||
}
|
||||
: footerConfig;
|
||||
|
||||
useEffect(() => {
|
||||
if (isProviderFlow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.DISCOVERY
|
||||
) {
|
||||
setFooterConfig({
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
onBack: () => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
},
|
||||
showAction: false,
|
||||
actionLabel: "Next",
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
});
|
||||
}
|
||||
}, [isProviderFlow, orgCurrentStep, validatePhase]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={handleDialogOpenChange}
|
||||
size="4xl"
|
||||
className="flex !h-[90vh] !max-h-[90vh] !min-h-[90vh] !max-w-[60vw] !min-w-[70vw] flex-col overflow-hidden p-8"
|
||||
>
|
||||
<DialogHeader className="gap-2 p-0">
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
Adding A Cloud Provider
|
||||
</DialogTitle>
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Info className="size-4 shrink-0" />
|
||||
<span>For assistance connecting a Cloud Provider visit</span>
|
||||
<Button variant="link" size="link-sm" className="h-auto p-0" asChild>
|
||||
<a href={docsLink} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
<span>Prowler Docs</span>
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-8 flex min-h-0 flex-1 justify-between overflow-hidden">
|
||||
<div className="box-border w-[328px] shrink-0">
|
||||
{isProviderFlow ? (
|
||||
<WizardStepper currentStep={currentStep} />
|
||||
) : (
|
||||
<WizardStepper
|
||||
currentStep={orgCurrentStep}
|
||||
stepOffset={1}
|
||||
hasConnectionErrors={hasConnectionErrors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="minimal-scrollbar ml-auto h-full w-full max-w-[620px] overflow-y-auto">
|
||||
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.CONNECT && (
|
||||
<ConnectStep
|
||||
onNext={() => setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS)}
|
||||
onSelectOrganizations={openOrganizationsFlow}
|
||||
onFooterChange={setFooterConfig}
|
||||
onProviderTypeChange={setProviderTypeHint}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProviderFlow &&
|
||||
currentStep === PROVIDER_WIZARD_STEP.CREDENTIALS && (
|
||||
<CredentialsStep
|
||||
onNext={() => setCurrentStep(PROVIDER_WIZARD_STEP.TEST)}
|
||||
onBack={() => setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT)}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.TEST && (
|
||||
<TestConnectionStep
|
||||
onSuccess={handleTestSuccess}
|
||||
onResetCredentials={() =>
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS)
|
||||
}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH && (
|
||||
<LaunchStep />
|
||||
)}
|
||||
|
||||
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.SETUP && (
|
||||
<OrgSetupForm
|
||||
onBack={backToProviderFlow}
|
||||
onNext={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow &&
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.DISCOVERY && (
|
||||
<OrgDiscoveryLoader
|
||||
onDiscoveryComplete={() => {
|
||||
setValidatePhase(VALIDATE_PHASE.SELECTION);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow &&
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.SELECTION && (
|
||||
<OrgAccountSelection
|
||||
onBack={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
}}
|
||||
onNext={() => {
|
||||
setValidatePhase(VALIDATE_PHASE.TESTING);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow &&
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.TESTING && (
|
||||
<OrgConnectionTest
|
||||
onBack={() => {
|
||||
setValidatePhase(VALIDATE_PHASE.SELECTION);
|
||||
}}
|
||||
onNext={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onSkip={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.LAUNCH && (
|
||||
<OrgLaunchScan
|
||||
onClose={handleClose}
|
||||
onBack={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE);
|
||||
setValidatePhase(VALIDATE_PHASE.TESTING);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(resolvedFooterConfig.showBack || resolvedFooterConfig.showAction) && (
|
||||
<div className="mt-8 pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
{resolvedFooterConfig.showBack && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xl"
|
||||
disabled={resolvedFooterConfig.backDisabled}
|
||||
onClick={resolvedFooterConfig.onBack}
|
||||
>
|
||||
{resolvedFooterConfig.backLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{resolvedFooterConfig.showAction && (
|
||||
<Button
|
||||
size="xl"
|
||||
type={
|
||||
resolvedFooterConfig.actionType ===
|
||||
WIZARD_FOOTER_ACTION_TYPE.SUBMIT
|
||||
? "submit"
|
||||
: "button"
|
||||
}
|
||||
form={resolvedFooterConfig.actionFormId}
|
||||
disabled={resolvedFooterConfig.actionDisabled}
|
||||
onClick={
|
||||
resolvedFooterConfig.actionType ===
|
||||
WIZARD_FOOTER_ACTION_TYPE.BUTTON
|
||||
? resolvedFooterConfig.onAction
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{resolvedFooterConfig.actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
ConnectAccountForm,
|
||||
ConnectAccountSuccessData,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
import {
|
||||
WIZARD_FOOTER_ACTION_TYPE,
|
||||
WizardFooterConfig,
|
||||
} from "./footer-controls";
|
||||
|
||||
interface ConnectStepProps {
|
||||
onNext: () => void;
|
||||
onSelectOrganizations: () => void;
|
||||
onFooterChange: (config: WizardFooterConfig) => void;
|
||||
onProviderTypeChange: (providerType: ProviderType | null) => void;
|
||||
}
|
||||
|
||||
export function ConnectStep({
|
||||
onNext,
|
||||
onSelectOrganizations,
|
||||
onFooterChange,
|
||||
onProviderTypeChange,
|
||||
}: ConnectStepProps) {
|
||||
const { setProvider, setVia, setSecretId, setMode } =
|
||||
useProviderWizardStore();
|
||||
const backHandlerRef = useRef<(() => void) | null>(null);
|
||||
const [uiState, setUiState] = useState({
|
||||
showBack: false,
|
||||
showAction: false,
|
||||
actionLabel: "Next",
|
||||
actionDisabled: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const formId = "provider-wizard-connect-form";
|
||||
|
||||
const handleSuccess = (data: ConnectAccountSuccessData) => {
|
||||
setProvider({
|
||||
id: data.id,
|
||||
type: data.providerType,
|
||||
uid: data.uid,
|
||||
alias: data.alias,
|
||||
});
|
||||
setVia(null);
|
||||
setSecretId(null);
|
||||
setMode(PROVIDER_WIZARD_MODE.ADD);
|
||||
onNext();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFooterChange({
|
||||
showBack: uiState.showBack,
|
||||
backLabel: "Back",
|
||||
backDisabled: uiState.isLoading,
|
||||
onBack: () => backHandlerRef.current?.(),
|
||||
showAction: uiState.showAction,
|
||||
actionLabel: uiState.actionLabel,
|
||||
actionDisabled: uiState.actionDisabled || uiState.isLoading,
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT,
|
||||
actionFormId: formId,
|
||||
});
|
||||
}, [onFooterChange, uiState]);
|
||||
|
||||
return (
|
||||
<ConnectAccountForm
|
||||
formId={formId}
|
||||
hideNavigation
|
||||
onSuccess={handleSuccess}
|
||||
onSelectOrganizations={onSelectOrganizations}
|
||||
onProviderTypeChange={onProviderTypeChange}
|
||||
onUiStateChange={setUiState}
|
||||
onBackHandlerChange={(handler) => {
|
||||
backHandlerRef.current = handler;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getProviderFormType } from "@/lib/provider-helpers";
|
||||
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
import {
|
||||
AddViaCredentialsForm,
|
||||
AddViaRoleForm,
|
||||
UpdateViaCredentialsForm,
|
||||
UpdateViaRoleForm,
|
||||
} from "../../workflow/forms";
|
||||
import { SelectViaAlibabaCloud } from "../../workflow/forms/select-credentials-type/alibabacloud";
|
||||
import { SelectViaAWS } from "../../workflow/forms/select-credentials-type/aws";
|
||||
import {
|
||||
AddViaServiceAccountForm,
|
||||
SelectViaGCP,
|
||||
} from "../../workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "../../workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "../../workflow/forms/select-credentials-type/m365";
|
||||
import { UpdateViaServiceAccountForm } from "../../workflow/forms/update-via-service-account-key-form";
|
||||
import {
|
||||
WIZARD_FOOTER_ACTION_TYPE,
|
||||
WizardFooterConfig,
|
||||
} from "./footer-controls";
|
||||
|
||||
interface CredentialsStepProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onFooterChange: (config: WizardFooterConfig) => void;
|
||||
}
|
||||
|
||||
export function CredentialsStep({
|
||||
onNext,
|
||||
onBack,
|
||||
onFooterChange,
|
||||
}: CredentialsStepProps) {
|
||||
const { providerId, providerType, providerUid, via, secretId, mode, setVia } =
|
||||
useProviderWizardStore();
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
|
||||
const formId = "provider-wizard-credentials-form";
|
||||
const hasProviderContext = Boolean(providerType && providerId);
|
||||
const formType =
|
||||
providerType && providerId
|
||||
? getProviderFormType(providerType, via || undefined)
|
||||
: null;
|
||||
const shouldUseUpdateForms =
|
||||
mode === PROVIDER_WIZARD_MODE.UPDATE && Boolean(secretId);
|
||||
|
||||
const handleBack = () => {
|
||||
if (via) {
|
||||
setVia(null);
|
||||
return;
|
||||
}
|
||||
onBack();
|
||||
};
|
||||
|
||||
const handleViaChange = (value: string) => {
|
||||
setVia(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsFormValid(false);
|
||||
}, [formType, via]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasProviderContext) {
|
||||
onFooterChange({
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
onBack,
|
||||
showAction: false,
|
||||
actionLabel: "Authenticate",
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelector = formType === "selector";
|
||||
|
||||
onFooterChange({
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
backDisabled: isFormLoading,
|
||||
onBack: () => {
|
||||
if (via) {
|
||||
setVia(null);
|
||||
return;
|
||||
}
|
||||
onBack();
|
||||
},
|
||||
showAction: !isSelector,
|
||||
actionLabel: "Authenticate",
|
||||
actionDisabled: isFormLoading || !isFormValid,
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT,
|
||||
actionFormId: formId,
|
||||
});
|
||||
}, [
|
||||
hasProviderContext,
|
||||
formType,
|
||||
formId,
|
||||
isFormLoading,
|
||||
isFormValid,
|
||||
onBack,
|
||||
onFooterChange,
|
||||
setVia,
|
||||
via,
|
||||
]);
|
||||
|
||||
if (!providerType || !providerId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center py-8">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Provider details are missing. Go back and select a provider.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (formType === "selector") {
|
||||
if (providerType === "aws") {
|
||||
return (
|
||||
<SelectViaAWS
|
||||
initialVia={via || undefined}
|
||||
onViaChange={handleViaChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (providerType === "gcp") {
|
||||
return (
|
||||
<SelectViaGCP
|
||||
initialVia={via || undefined}
|
||||
onViaChange={handleViaChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (providerType === "github") {
|
||||
return (
|
||||
<SelectViaGitHub
|
||||
initialVia={via || undefined}
|
||||
onViaChange={handleViaChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (providerType === "m365") {
|
||||
return (
|
||||
<SelectViaM365
|
||||
initialVia={via || undefined}
|
||||
onViaChange={handleViaChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (providerType === "alibabacloud") {
|
||||
return (
|
||||
<SelectViaAlibabaCloud
|
||||
initialVia={via || undefined}
|
||||
onViaChange={handleViaChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonFormProps = {
|
||||
via,
|
||||
onSuccess: onNext,
|
||||
onBack: handleBack,
|
||||
providerUid: providerUid || undefined,
|
||||
formId,
|
||||
hideActions: true,
|
||||
onLoadingChange: setIsFormLoading,
|
||||
onValidityChange: setIsFormValid,
|
||||
};
|
||||
|
||||
if (formType === "credentials") {
|
||||
if (shouldUseUpdateForms) {
|
||||
return (
|
||||
<UpdateViaCredentialsForm
|
||||
searchParams={{
|
||||
type: providerType,
|
||||
id: providerId,
|
||||
secretId: secretId || undefined,
|
||||
}}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddViaCredentialsForm
|
||||
searchParams={{ type: providerType, id: providerId }}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (formType === "role") {
|
||||
if (shouldUseUpdateForms) {
|
||||
return (
|
||||
<UpdateViaRoleForm
|
||||
searchParams={{
|
||||
type: providerType,
|
||||
id: providerId,
|
||||
secretId: secretId || undefined,
|
||||
}}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddViaRoleForm
|
||||
searchParams={{ type: providerType, id: providerId }}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (formType === "service-account") {
|
||||
if (shouldUseUpdateForms) {
|
||||
return (
|
||||
<UpdateViaServiceAccountForm
|
||||
searchParams={{
|
||||
type: providerType,
|
||||
id: providerId,
|
||||
secretId: secretId || undefined,
|
||||
}}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddViaServiceAccountForm
|
||||
searchParams={{ type: providerType as ProviderType, id: providerId }}
|
||||
{...commonFormProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-6">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Select a credential type to continue.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export const WIZARD_FOOTER_ACTION_TYPE = {
|
||||
BUTTON: "button",
|
||||
SUBMIT: "submit",
|
||||
} as const;
|
||||
|
||||
export type WizardFooterActionType =
|
||||
(typeof WIZARD_FOOTER_ACTION_TYPE)[keyof typeof WIZARD_FOOTER_ACTION_TYPE];
|
||||
|
||||
export interface WizardFooterConfig {
|
||||
showBack: boolean;
|
||||
backLabel: string;
|
||||
backDisabled?: boolean;
|
||||
onBack?: () => void;
|
||||
showAction: boolean;
|
||||
actionLabel: string;
|
||||
actionDisabled?: boolean;
|
||||
actionType: WizardFooterActionType;
|
||||
actionFormId?: string;
|
||||
onAction?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./connect-step";
|
||||
export * from "./credentials-step";
|
||||
export * from "./footer-controls";
|
||||
export * from "./launch-step";
|
||||
export * from "./test-connection-step";
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
|
||||
export function LaunchStep() {
|
||||
return (
|
||||
<div className="flex min-h-[320px] flex-col items-center justify-center gap-4 text-center">
|
||||
<CheckCircle2 className="text-success size-12" />
|
||||
<h3 className="text-xl font-semibold">Provider connected successfully</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Continue with the action button to go to scans.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getProvider } from "@/actions/providers";
|
||||
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
|
||||
import {
|
||||
TestConnectionForm,
|
||||
TestConnectionProviderData,
|
||||
} from "../../workflow/forms/test-connection-form";
|
||||
import {
|
||||
WIZARD_FOOTER_ACTION_TYPE,
|
||||
WizardFooterConfig,
|
||||
} from "./footer-controls";
|
||||
|
||||
interface TestConnectionStepProps {
|
||||
onSuccess: () => void;
|
||||
onResetCredentials: () => void;
|
||||
onFooterChange: (config: WizardFooterConfig) => void;
|
||||
}
|
||||
|
||||
export function TestConnectionStep({
|
||||
onSuccess,
|
||||
onResetCredentials,
|
||||
onFooterChange,
|
||||
}: TestConnectionStepProps) {
|
||||
const { providerId, providerType, mode } = useProviderWizardStore();
|
||||
const [providerData, setProviderData] =
|
||||
useState<TestConnectionProviderData | null>(null);
|
||||
const [isLoadingProvider, setIsLoadingProvider] = useState(true);
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const formId = "provider-wizard-test-connection-form";
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function loadProvider() {
|
||||
if (!providerId || !providerType) {
|
||||
setErrorMessage("Provider information is missing.");
|
||||
setIsLoadingProvider(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingProvider(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
|
||||
const response = await getProvider(formData);
|
||||
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response?.errors?.length) {
|
||||
setErrorMessage(
|
||||
response.errors[0]?.detail || "Failed to load provider.",
|
||||
);
|
||||
setProviderData(null);
|
||||
setIsLoadingProvider(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setProviderData(response as TestConnectionProviderData);
|
||||
setIsLoadingProvider(false);
|
||||
}
|
||||
|
||||
loadProvider();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [providerId, providerType]);
|
||||
|
||||
useEffect(() => {
|
||||
const canSubmit = !isLoadingProvider && !errorMessage && !!providerData;
|
||||
|
||||
onFooterChange({
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
backDisabled: isFormLoading,
|
||||
onBack: onResetCredentials,
|
||||
showAction: canSubmit,
|
||||
actionLabel:
|
||||
mode === PROVIDER_WIZARD_MODE.UPDATE
|
||||
? "Check connection"
|
||||
: "Launch scan",
|
||||
actionDisabled: isFormLoading,
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT,
|
||||
actionFormId: formId,
|
||||
});
|
||||
}, [
|
||||
errorMessage,
|
||||
isFormLoading,
|
||||
isLoadingProvider,
|
||||
mode,
|
||||
onFooterChange,
|
||||
onResetCredentials,
|
||||
providerData,
|
||||
]);
|
||||
|
||||
if (isLoadingProvider) {
|
||||
return (
|
||||
<div className="flex min-h-[320px] items-center justify-center">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage || !providerData || !providerId || !providerType) {
|
||||
return (
|
||||
<div className="flex min-h-[320px] flex-col items-center justify-center gap-4 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{errorMessage || "Unable to load provider details."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TestConnectionForm
|
||||
formId={formId}
|
||||
hideActions
|
||||
onLoadingChange={setIsFormLoading}
|
||||
searchParams={{
|
||||
type: providerType,
|
||||
id: providerId,
|
||||
updated: mode === PROVIDER_WIZARD_MODE.UPDATE ? "true" : "false",
|
||||
}}
|
||||
providerData={providerData}
|
||||
onSuccess={onSuccess}
|
||||
onResetCredentials={onResetCredentials}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { getProviderName } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderLogo } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderHelpText } from "@/lib";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
export const ProviderTitleDocs = ({
|
||||
@@ -12,27 +10,13 @@ export const ProviderTitleDocs = ({
|
||||
providerType: ProviderType;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="flex gap-4">
|
||||
{providerType && getProviderLogo(providerType as ProviderType)}
|
||||
<span className="text-lg font-semibold">
|
||||
{providerType
|
||||
? getProviderName(providerType as ProviderType)
|
||||
: "Unknown Provider"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-end gap-x-2">
|
||||
<p className="text-default-500 text-sm">
|
||||
{getProviderHelpText(providerType as string).text}
|
||||
</p>
|
||||
<CustomLink
|
||||
href={getProviderHelpText(providerType as string).link}
|
||||
size="sm"
|
||||
className="text-nowrap"
|
||||
>
|
||||
Read the docs
|
||||
</CustomLink>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
{providerType && getProviderLogo(providerType as ProviderType)}
|
||||
<span className="text-lg font-semibold">
|
||||
{providerType
|
||||
? getProviderName(providerType as ProviderType)
|
||||
: "Unknown Provider"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,39 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ProviderWizardModal } from "@/components/providers/wizard";
|
||||
import { Button, Card, CardContent } from "@/components/shadcn";
|
||||
|
||||
import { InfoIcon } from "../icons/Icons";
|
||||
|
||||
export const NoProvidersAdded = () => {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Card variant="base" className="mx-auto w-full max-w-3xl">
|
||||
<CardContent className="flex flex-col items-center gap-4 p-6 text-center sm:p-8">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<InfoIcon className="h-10 w-10 text-gray-800 dark:text-white" />
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
|
||||
No Cloud Providers Configured
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-md leading-relaxed text-gray-600 dark:text-gray-300">
|
||||
No cloud providers have been configured. Start by setting up a
|
||||
cloud provider.
|
||||
</p>
|
||||
</div>
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
<Button
|
||||
asChild
|
||||
aria-label="Go to Add Cloud Provider page"
|
||||
className="w-full max-w-xs justify-center"
|
||||
size="lg"
|
||||
>
|
||||
<Link href="/providers/connect-account">Get Started</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Card variant="base" className="mx-auto w-full max-w-3xl">
|
||||
<CardContent className="flex flex-col items-center gap-4 p-6 text-center sm:p-8">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<InfoIcon className="h-10 w-10 text-gray-800 dark:text-white" />
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
|
||||
No Cloud Providers Configured
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-md leading-relaxed text-gray-600 dark:text-gray-300">
|
||||
No cloud providers have been configured. Start by setting up a
|
||||
cloud provider.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
aria-label="Open Add Cloud Provider modal"
|
||||
className="w-full max-w-xs justify-center"
|
||||
size="lg"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<ProviderWizardModal open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user