- Essential Project Config
+ Project Config
These values are required to connect your CI/CD pipeline or
@@ -225,6 +327,7 @@ const Index: FunctionComponent = () => {
frontendUrl={config.frontendUrl}
devguardCIComponentBase={config.devguardCIComponentBase}
devguardWebLatestScannerImage={latestScannerImage}
+ initialSlide={riskScanningInitialSlide}
/>
= {
@@ -331,7 +331,7 @@ const columnsDef: ColumnDef<
const Index: FunctionComponent = () => {
const assetMenu = useAssetMenu();
- const { startTour } = usePageTour(dependencyInsightsTourSteps);
+ useAutoTour("dependency-insights", dependencyInsightsTourSteps);
const [showSBOMModal, setShowSBOMModal] = useState(false);
const [showVexModal, setShowVexModal] = useState(false);
diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependency-risks/[vulnId]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependency-risks/[vulnId]/page.tsx
index f50d4a3bd..86b8cdabc 100644
--- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependency-risks/[vulnId]/page.tsx
+++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependency-risks/[vulnId]/page.tsx
@@ -82,6 +82,8 @@ import { fetcher } from "../../../../../../../../../../../data-fetcher/fetcher";
import { useActiveAssetVersion } from "../../../../../../../../../../../hooks/useActiveAssetVersion";
import useDecodedParams from "../../../../../../../../../../../hooks/useDecodedParams";
import type { ViewDependencyTreeNode } from "../../../../../../../../../../../utils/dependencyGraphHelpers";
+import { useTourSeen } from "@/hooks/useTourSeen";
+import { DocDrawer } from "@/components/common/DocDrawer";
import { convertPathsToTree } from "../../../../../../../../../../../utils/dependencyGraphHelpers";
const MarkdownEditor = dynamic(
@@ -319,6 +321,8 @@ const Index: FunctionComponent = () => {
const searchParams = useSearchParams();
const { startTour, registerSteps } = usePageTour(dependencyRiskTourSteps);
+ const { showModal: shouldStartTour, markSeen } =
+ useTourSeen("dependency-risk");
const [
acceptVexRuleRecommendationDialogOpen,
setAcceptVexRuleRecommendationDialogOpen,
@@ -377,20 +381,27 @@ const Index: FunctionComponent = () => {
);
const handleGraphReady = useCallback(() => {
- if (searchParams?.get("startTour") !== "4") return;
+ if (
+ searchParams?.get("startTour") !== "dependency-risk" &&
+ !shouldStartTour
+ )
+ return;
+ markSeen();
registerSteps(dependencyRiskTourSteps);
startTour();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchParams]);
+ }, [searchParams, shouldStartTour]);
// Path explosion: no graph is rendered so onReady never fires — start tour directly
useEffect(() => {
if (
- searchParams?.get("startTour") !== "4" ||
+ (searchParams?.get("startTour") !== "dependency-risk" &&
+ !shouldStartTour) ||
graphLoading ||
(vuln?.vulnerabilityPath.length || 0) !== 0
)
return;
+ markSeen();
registerSteps([
{
...dependencyRiskTourSteps[0],
@@ -401,7 +412,7 @@ const Index: FunctionComponent = () => {
]);
startTour();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [graphLoading]);
+ }, [graphLoading, shouldStartTour]);
const graphData = useMemo(() => {
if (!vuln || vuln.vulnerabilityPath.length === 0) {
diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/page.tsx
index fec0f9fa4..fa527965f 100644
--- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/page.tsx
+++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/page.tsx
@@ -10,11 +10,11 @@ import { useActiveOrg } from "@/hooks/useActiveOrg";
import { useActiveProject } from "@/hooks/useActiveProject";
import { useAssetMenu } from "@/hooks/useAssetMenu";
import { useViewMode } from "@/hooks/useViewMode";
-import { usePageTour } from "@/hooks/usePageTour";
+import { useAutoTour } from "@/hooks/useAutoTour";
import { repoHomeTourSteps } from "@/components/common/tours/repo-home-tour";
import "@xyflow/react/dist/style.css";
import { usePathname, useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
+import { useMemo } from "react";
import type { FunctionComponent } from "react";
import {
Card,
@@ -140,13 +140,7 @@ const Index: FunctionComponent = () => {
const searchParams = useSearchParams();
const artifactName = searchParams?.get("artifact") ?? "";
- const { startTour } = usePageTour(repoHomeTourSteps);
- useEffect(() => {
- if (searchParams?.get("startTour") === "3") {
- startTour();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ useAutoTour("repo-home", repoHomeTourSteps);
const pathname = usePathname();
diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings/page.tsx
index f3fd69f9b..866b35793 100644
--- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings/page.tsx
+++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings/page.tsx
@@ -40,7 +40,7 @@ import DateString from "../../../../../../../../components/common/DateString";
import Section from "@/components/common/Section";
import { Card } from "@/components/ui/card";
import Link from "next/link";
-import { usePageTour } from "@/hooks/usePageTour";
+import { useAutoTour } from "@/hooks/useAutoTour";
import { repoSettingsTourSteps } from "@/components/common/tours/repo-settings-tour";
const firstOrUndefined = (el?: number[]): number | undefined => {
@@ -313,7 +313,7 @@ const Index: FunctionComponent = () => {
const { parentRepositoryId, parentRepositoryName } =
getParentRepositoryIdAndName(project);
- usePageTour(repoSettingsTourSteps);
+ useAutoTour("repo-settings", repoSettingsTourSteps);
return (
(
@@ -133,14 +133,7 @@ export default function RepositoriesPage() {
() => groupHomeTourSteps(isAdmin(currentUserRole)),
[currentUserRole],
);
- const { startTour } = usePageTour(tourSteps);
-
- useEffect(() => {
- if (searchParams?.get("startTour") === "2") {
- startTour();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ useAutoTour("group-home", tourSteps);
const debouncedHandleSearch = useCallback(
debounce((e: React.ChangeEvent) => {
@@ -205,6 +198,8 @@ export default function RepositoriesPage() {
const res: AssetDTO & {
env: Array;
} = await resp.json();
+ setShowModal(false);
+ form.reset();
// navigate to the new application
router.push(
`/${activeOrg.slug}/projects/${project.slug}/assets/${res.slug}`,
diff --git a/src/app/(loading-group)/[organizationSlug]/settings/page.tsx b/src/app/(loading-group)/[organizationSlug]/settings/page.tsx
index 5d50ce7f7..0f8bd6529 100644
--- a/src/app/(loading-group)/[organizationSlug]/settings/page.tsx
+++ b/src/app/(loading-group)/[organizationSlug]/settings/page.tsx
@@ -14,7 +14,7 @@
// along with this program. If not, see .
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import Page from "../../../../components/Page";
import GithubAppInstallationAlert from "@/components/common/GithubAppInstallationAlert";
@@ -59,7 +59,7 @@ import {
useUpdateOrganization,
} from "../../../../context/OrganizationContext";
import Alert from "../../../../components/common/Alert";
-import { usePageTour } from "@/hooks/usePageTour";
+import { useAutoTour } from "@/hooks/useAutoTour";
import { orgSettingsTourSteps } from "@/components/common/tours/org-settings-tour";
const Home = () => {
@@ -278,7 +278,7 @@ const Home = () => {
}
};
- const { startTour } = usePageTour(orgSettingsTourSteps);
+ useAutoTour("org-settings", orgSettingsTourSteps);
const config = useConfig();
diff --git a/src/components/RiskScannerDialog.tsx b/src/components/RiskScannerDialog.tsx
index b47b31bce..c1381b159 100644
--- a/src/components/RiskScannerDialog.tsx
+++ b/src/components/RiskScannerDialog.tsx
@@ -55,6 +55,7 @@ interface RiskScannerDialogProps {
assetVersion?: AssetVersionDTO;
artifacts?: Array;
devguardWebLatestScannerImage: string;
+ initialSlide?: number;
}
const RiskScannerDialog: FunctionComponent = ({
@@ -66,6 +67,7 @@ const RiskScannerDialog: FunctionComponent = ({
assetVersion,
artifacts,
devguardWebLatestScannerImage,
+ initialSlide,
}) => {
const [api, setApi] = React.useState<{
reInit: () => void;
@@ -459,6 +461,9 @@ const RiskScannerDialog: FunctionComponent = ({
if (!asset.externalEntityId && !asset.repositoryProvider) {
return 0; // start with the update repository provider slide
}
+ if (initialSlide !== undefined) {
+ return initialSlide;
+ }
if (asset.repositoryProvider === "github") {
// we can skip setup method selection slide - and the whole autosetup slides
return 6;
@@ -480,10 +485,16 @@ const RiskScannerDialog: FunctionComponent = ({
return activeOrg.gitLabIntegrations.length > 0 ? 3 : 2;
};
- // save the slide history to make the back button implementation easier
const [slideHistory, setSlideHistory] = useState([getStartIndex()]);
- const prevIndex = slideHistory[slideHistory.length - 2] || 0;
+ useEffect(() => {
+ if (open) {
+ setSlideHistory([getStartIndex()]);
+ }
+ }, [open, initialSlide]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const prevIndex =
+ slideHistory[slideHistory.length - 2] ?? slideHistory[0] ?? 0;
const setProxyApi = useCallback((emblaApi: CarouselApi) => {
return setApi({
@@ -635,10 +646,8 @@ const RiskScannerDialog: FunctionComponent = ({
prevIndex={prevIndex}
/>
diff --git a/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx
index c5e775ffa..73a7c99bd 100644
--- a/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx
+++ b/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx
@@ -120,13 +120,6 @@ const AutomatedIntegrationSlide: FunctionComponent<
/>
-
api?.scrollTo(prevIndex)}
- >
- Back
-
Finish Setup
diff --git a/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx
index 5e6ee57d0..6fdcf1885 100644
--- a/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx
+++ b/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx
@@ -137,15 +137,6 @@ ${generateDockerSnippet(scannerImage, "sast", org.slug, project.slug, asset.slug
-
{
- api?.scrollTo(prevIndex);
- }}
- >
- Back
-
Finish
diff --git a/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx
index b195fd7b5..33b12b35b 100644
--- a/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx
+++ b/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx
@@ -17,9 +17,7 @@ import {
DocumentArrowUpIcon,
} from "@heroicons/react/24/outline";
import type { FunctionComponent } from "react";
-import { classNames } from "../../../utils/common";
import { Badge } from "../../ui/badge";
-import { Button } from "../../ui/button";
import { Card, CardDescription, CardHeader, CardTitle } from "../../ui/card";
import { CarouselItem } from "../../ui/carousel";
import { DialogHeader, DialogTitle } from "../../ui/dialog";
@@ -28,8 +26,6 @@ interface IntegrationMethodSelectionSlideProps {
api?: {
scrollTo: (index: number) => void;
};
- variant: "manual" | "auto";
- prevIndex: number;
setVariant: (variant: "manual" | "auto") => void;
cliSlideIndex: number;
fileUploadSlideIndex: number;
@@ -37,14 +33,7 @@ interface IntegrationMethodSelectionSlideProps {
const IntegrationMethodSelectionSlide: FunctionComponent<
IntegrationMethodSelectionSlideProps
-> = ({
- api,
- variant,
- setVariant,
- prevIndex,
- cliSlideIndex,
- fileUploadSlideIndex,
-}) => {
+> = ({ api, setVariant, cliSlideIndex, fileUploadSlideIndex }) => {
return (
@@ -53,11 +42,11 @@ const IntegrationMethodSelectionSlide: FunctionComponent<
setVariant("auto")}
+ className="cursor-pointer"
+ onClick={() => {
+ setVariant("auto");
+ api?.scrollTo(cliSlideIndex);
+ }}
>
@@ -74,13 +63,14 @@ const IntegrationMethodSelectionSlide: FunctionComponent<
setVariant("manual")}
+ className="cursor-pointer mt-2"
+ data-testid="upload-manually"
+ onClick={() => {
+ setVariant("manual");
+ api?.scrollTo(fileUploadSlideIndex);
+ }}
>
-
+
Upload manually
@@ -95,25 +85,6 @@ const IntegrationMethodSelectionSlide: FunctionComponent<
-
- api?.scrollTo(prevIndex)}
- >
- Back
-
-
- api?.scrollTo(
- variant === "auto" ? cliSlideIndex : fileUploadSlideIndex,
- )
- }
- >
- Continue
-
-
);
diff --git a/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx
index 67c753584..de765f11f 100644
--- a/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx
+++ b/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx
@@ -510,13 +510,6 @@ const ScannerOptionsSelectionSlide: FunctionComponent<
-
api?.scrollTo(prevIndex)}
- >
- Back
-
v === false)}
id="scanner-options-selection-continue"
diff --git a/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx
index 457412a6a..3fe351b30 100644
--- a/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx
+++ b/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx
@@ -111,9 +111,10 @@ export default function ScannerSelectionSlide({
"cursor-pointer mt-2",
selectedSetup === "own-setup" ? "border-primary" : "",
)}
+ data-testid="own-setup-card"
onClick={() => setSelectedSetup("own-setup")}
>
-
+
- {
- api?.scrollTo(prevIndex);
- }}
- >
- Back
-
{
+ if (started.current) return;
+ if (!notSeen && !triggeredByParam) return;
+ started.current = true;
+ markSeen();
+ startTour();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [notSeen, triggeredByParam]);
+
+ return { startTour };
+}
diff --git a/src/hooks/useTourSeen.ts b/src/hooks/useTourSeen.ts
new file mode 100644
index 000000000..40eeef94b
--- /dev/null
+++ b/src/hooks/useTourSeen.ts
@@ -0,0 +1,43 @@
+"use client";
+
+import { useState } from "react";
+
+const ALL_TOUR_KEYS = [
+ "org-home",
+ "org-settings",
+ "org-overview",
+ "group-home",
+ "repo-home",
+ "repo-settings",
+ "dependency-risk",
+ "dependency-insights",
+] as const;
+
+const storageKey = (tourKey: string) => `devguard:tourSeen:${tourKey}`;
+
+export const dismissAllTours = () => {
+ ALL_TOUR_KEYS.forEach((key) => {
+ localStorage.setItem(storageKey(key), "true");
+ });
+};
+
+export const hasTourSeen = (tourKey: string): boolean => {
+ if (typeof window === "undefined") return false;
+ return localStorage.getItem(storageKey(tourKey)) === "true";
+};
+
+export function useTourSeen(tourKey: string) {
+ const key = storageKey(tourKey);
+
+ const [showModal, setShowModal] = useState(() => {
+ if (typeof window === "undefined") return false;
+ return localStorage.getItem(key) !== "true";
+ });
+
+ const markSeen = () => {
+ localStorage.setItem(key, "true");
+ setShowModal(false);
+ };
+
+ return { showModal, markSeen };
+}
diff --git a/src/hooks/useWelcomeTour.ts b/src/hooks/useWelcomeTour.ts
index c8128b36a..10a9e290f 100644
--- a/src/hooks/useWelcomeTour.ts
+++ b/src/hooks/useWelcomeTour.ts
@@ -1,37 +1,18 @@
-// TODO: später mit DB verknüpfen, damit Modal nur einmal pro User angezeigt wird
"use client";
-import { useEffect, useState } from "react";
-
-const STORAGE_KEY = "devguard:welcomeTourSeen";
-
-const hasSeenWelcome = (): boolean => {
- if (typeof window === "undefined") return false;
- return localStorage.getItem(STORAGE_KEY) === "true";
-};
-
-const markWelcomeSeen = () => {
- localStorage.setItem(STORAGE_KEY, "true");
-};
+import { dismissAllTours, useTourSeen } from "./useTourSeen";
export function useWelcomeTour() {
- const [showModal, setShowModal] = useState(false);
-
- useEffect(() => {
- if (!hasSeenWelcome()) {
- setShowModal(true);
- }
- }, []);
+ const { showModal, markSeen } = useTourSeen("org-home");
const handleStartTour = (startTour: () => void) => {
- markWelcomeSeen();
- setShowModal(false);
+ markSeen();
startTour();
};
const handleSkip = () => {
- markWelcomeSeen();
- setShowModal(false);
+ dismissAllTours();
+ markSeen();
};
return {