From 08a88328b1a0b23e09084dcefcf545ad62b66251 Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 27 May 2026 16:27:39 +0200 Subject: [PATCH 01/14] chore: reworked the onboarding card and tour starts on first sight --- .../[organizationSlug]/overview/page.tsx | 12 +- .../[projectSlug]/assets/[assetSlug]/page.tsx | 203 +++++++++++------- .../[assetVersionSlug]/dependencies/page.tsx | 12 +- .../dependency-risks/[vulnId]/page.tsx | 12 +- .../refs/[assetVersionSlug]/page.tsx | 8 +- .../assets/[assetSlug]/settings/page.tsx | 10 + .../projects/[projectSlug]/page.tsx | 7 +- .../[organizationSlug]/settings/page.tsx | 12 +- src/components/RiskScannerDialog.tsx | 5 + src/hooks/useTourSeen.ts | 26 +++ src/hooks/useWelcomeTour.ts | 28 +-- 11 files changed, 217 insertions(+), 118 deletions(-) create mode 100644 src/hooks/useTourSeen.ts diff --git a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx index 28f34b0d..cf70e37b 100644 --- a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx @@ -1,6 +1,6 @@ "use client"; -import type { FunctionComponent } from "react"; +import { type FunctionComponent, useEffect } from "react"; import Page from "@/components/Page"; import { useViewMode } from "@/hooks/useViewMode"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -17,6 +17,7 @@ import OrganizationCompositionSection from "@/components/organization/Organizati import TotalVulnerabilitiesSection from "@/components/organization/TotalVulnerabilitiesSection"; import { usePageTour } from "@/hooks/usePageTour"; import { orgOverviewTourSteps } from "@/components/common/tours/org-overview-tour"; +import { useTourSeen } from "@/hooks/useTourSeen"; const STATS_PARAMS = new URLSearchParams({ orgComponentsLimit: "5", @@ -32,6 +33,15 @@ const OrganizationOverview: FunctionComponent = () => { const orgMenu = useOrganizationMenu(); const [mode, setMode] = useViewMode("devguard-org-view-mode"); const { startTour } = usePageTour(orgOverviewTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("org-overview"); + + useEffect(() => { + if (shouldStartTour) { + markSeen(); + startTour(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldStartTour]); const { data: orgStatistics, diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx index 6c321737..057df143 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx @@ -6,14 +6,11 @@ import WebhookSetupTicketIntegrationDialog from "@/components/guides/WebhookSetu import Page from "@/components/Page"; import { useAssetMenu } from "@/hooks/useAssetMenu"; import "@xyflow/react/dist/style.css"; -import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import type { FunctionComponent } from "react"; import Autosetup from "../../../../../../../components/Autosetup"; -import ListItem from "../../../../../../../components/common/ListItem"; import RiskScannerDialog from "../../../../../../../components/RiskScannerDialog"; -import { Button } from "../../../../../../../components/ui/button"; import { useAsset } from "../../../../../../../context/AssetContext"; import { useConfig } from "../../../../../../../context/ConfigContext"; import { useAutosetup } from "../../../../../../../hooks/useAutosetup"; @@ -21,6 +18,9 @@ import useDecodedParams from "../../../../../../../hooks/useDecodedParams"; import { externalProviderIdToIntegrationName } from "../../../../../../../utils/externalProvider"; import { isLoggedIn, useCurrentUserRole } from "@/hooks/useUserRole"; import usePersonalAccessToken from "@/hooks/usePersonalAccessToken"; +import { SearchCode, Code, Blocks, Upload, Link2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import useScannerImage from "../../../../../../../hooks/useScannerImage"; import { InputWithButton } from "@/components/ui/input-with-button"; import { Card, CardContent } from "@/components/ui/card"; import { @@ -30,15 +30,15 @@ import { } from "@/components/ui/collapsible"; import { ChevronDownIcon } from "lucide-react"; import Link from "next/link"; -import useScannerImage from "../../../../../../../hooks/useScannerImage"; const Index: FunctionComponent = () => { const { pat, onCreatePat } = usePersonalAccessToken(); const assetMenu = useAssetMenu(); - - const role = useCurrentUserRole(); const [riskScanningIsOpen, setRiskScanningOpen] = useState(false); + const [riskScanningInitialSlide, setRiskScanningInitialSlide] = useState< + number | undefined + >(undefined); const [webhookIsOpen, setWebhookIsOpen] = useState(false); const config = useConfig(); const latestScannerImage = useScannerImage(); @@ -99,18 +99,12 @@ const Index: FunctionComponent = () => { Title={} > {isLoggedIn(role) ? ( -
+
{((asset?.externalEntityProviderId && externalProviderIdToIntegrationName( asset.externalEntityProviderId, ) === "gitlab") || - (asset?.repositoryProvider === "gitlab" && - asset?.repositoryId)) && ( + asset?.repositoryProvider === "gitlab") && ( <>
@@ -120,79 +114,121 @@ const Index: FunctionComponent = () => {
)} -
- - -
- } - /> -
-
- - Connect your Issue Tracker to DevGuard{" "} - GitLab Logo - GitLab Logo - GitLab Logo + +
+
+ +
+
+ + Check your Code for Risks - } - Description={ - "You can connect your Issue Tracker to DevGuard to automatically create issues for identified risks. You can handle findings directly from your issue tracker via slash commands. This way, you can easily track and mitigate vulnerabilities, bad-practices, license issues and more." - } - Button={ -
- +
+ Scan your code, dependencies, and infrastructure for + vulnerabilities, leaked secrets, bad practices, and license + issues.
Connect your CI/CD pipeline to scan on every + push.
- } - /> -
- - + {(() => { + const isGitLab = + (asset?.externalEntityProviderId && + externalProviderIdToIntegrationName( + asset.externalEntityProviderId, + ) === "gitlab") || + asset?.repositoryProvider === "gitlab"; + + // Slide indices in RiskScannerDialog carousel: + // 7 = ScannerOptionsSelectionSlide (CI/CD tools) + // 16 = DevGuardCliSlide + // 11 = IntegrationMethodSelectionSlide (manual upload) + // 15 = SetupInformationSourceSlide (supplier URL) + const allCards = [ + { + icon: , + name: "DevGuard CI/CD Integration", + sub: "From our curated list of scans and scanners, select the ones you want to use.", + recommended: true, + githubOnly: false, + slide: 7, + }, + { + icon: , + name: "DevGuard CLI", + sub: "Use the devguard cli to run scans and upload the results to Devguard.", + recommended: false, + githubOnly: false, + slide: 16, + }, + { + icon: , + name: "Manually Upload", + sub: "You already have a Scanner or a SARIF/SBOM file and want to just upload your results...", + recommended: false, + githubOnly: true, + slide: 11, + }, + { + icon: , + name: "Supplier provided URL", + sub: "Provide an SBOM URLs to setup Devguard based on external data sources. This data will be periodically fetched and updated.", + recommended: false, + githubOnly: false, + slide: 15, + }, + ]; + + const cards = allCards.filter((c) => + isGitLab ? !c.githubOnly : true, + ); + + return ( +
+ {cards.map(({ icon, name, sub, recommended, slide }) => ( +
{ + setRiskScanningInitialSlide(slide); + setRiskScanningOpen(true); + }} + > +
+ {icon} + {recommended && ( + + Recommended + + )} +
+ + {name} + + + {sub} + +
+ ))} +
+ ); + })()} +
+
+
+ +

- Essential Project Config + Project Config

These values are required to connect your CI/CD pipeline or @@ -282,6 +318,7 @@ const Index: FunctionComponent = () => { frontendUrl={config.frontendUrl} devguardCIComponentBase={config.devguardCIComponentBase} devguardWebLatestScannerImage={latestScannerImage} + initialSlide={riskScanningInitialSlide} /> = { bad: [null, 3], @@ -332,6 +333,15 @@ const columnsDef: ColumnDef< const Index: FunctionComponent = () => { const assetMenu = useAssetMenu(); const { startTour } = usePageTour(dependencyInsightsTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("dependency-insights"); + + useEffect(() => { + if (shouldStartTour) { + markSeen(); + startTour(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldStartTour]); 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 654ee8a6..d2163622 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 @@ -86,6 +86,7 @@ import { useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { usePageTour } from "@/hooks/usePageTour"; import { dependencyRiskTourSteps } from "@/components/common/tours/dependency-risk-tour"; +import { useTourSeen } from "@/hooks/useTourSeen"; import { DocDrawer } from "@/components/common/DocDrawer"; const MarkdownEditor = dynamic( @@ -324,6 +325,7 @@ const Index: FunctionComponent = () => { const searchParams = useSearchParams(); const { startTour, registerSteps } = usePageTour(dependencyRiskTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("dependency-risk"); const [ acceptVexRuleRecommendationDialogOpen, setAcceptVexRuleRecommendationDialogOpen, @@ -382,20 +384,22 @@ const Index: FunctionComponent = () => { ); const handleGraphReady = useCallback(() => { - if (searchParams?.get("startTour") !== "4") return; + if (searchParams?.get("startTour") !== "4" && !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") !== "4" && !shouldStartTour) || graphLoading || (vuln?.vulnerabilityPath.length || 0) !== 0 ) return; + markSeen(); registerSteps([ { ...dependencyRiskTourSteps[0], @@ -406,7 +410,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 b6aafe70..8fcffc33 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 @@ -12,6 +12,7 @@ import { useAssetMenu } from "@/hooks/useAssetMenu"; import { useViewMode } from "@/hooks/useViewMode"; import { usePageTour } from "@/hooks/usePageTour"; import { repoHomeTourSteps } from "@/components/common/tours/repo-home-tour"; +import { useTourSeen } from "@/hooks/useTourSeen"; import "@xyflow/react/dist/style.css"; import { usePathname, useSearchParams } from "next/navigation"; import { useEffect, useMemo } from "react"; @@ -141,12 +142,15 @@ const Index: FunctionComponent = () => { const artifactName = searchParams?.get("artifact") ?? ""; const { startTour } = usePageTour(repoHomeTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("repo-home"); + useEffect(() => { - if (searchParams?.get("startTour") === "3") { + if (searchParams?.get("startTour") === "3" || shouldStartTour) { + markSeen(); startTour(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [shouldStartTour]); 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 9b9902f9..110e24ed 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 @@ -42,6 +42,7 @@ import { Card } from "@/components/ui/card"; import Link from "next/link"; import { usePageTour } from "@/hooks/usePageTour"; import { repoSettingsTourSteps } from "@/components/common/tours/repo-settings-tour"; +import { useTourSeen } from "@/hooks/useTourSeen"; const firstOrUndefined = (el?: number[]): number | undefined => { if (!el) { @@ -314,6 +315,15 @@ const Index: FunctionComponent = () => { getParentRepositoryIdAndName(project); const { startTour } = usePageTour(repoSettingsTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("repo-settings"); + + useEffect(() => { + if (shouldStartTour) { + markSeen(); + startTour(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldStartTour]); return ( ( @@ -138,13 +139,15 @@ export default function RepositoriesPage() { [currentUserRole], ); const { startTour } = usePageTour(tourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("group-home"); useEffect(() => { - if (searchParams?.get("startTour") === "2") { + if (searchParams?.get("startTour") === "2" || shouldStartTour) { + markSeen(); startTour(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [shouldStartTour]); const debouncedHandleSearch = useCallback( debounce((e: React.ChangeEvent) => { diff --git a/src/app/(loading-group)/[organizationSlug]/settings/page.tsx b/src/app/(loading-group)/[organizationSlug]/settings/page.tsx index e09c9bea..ab567ba5 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"; @@ -61,6 +61,7 @@ import { import Alert from "../../../../components/common/Alert"; import { usePageTour } from "@/hooks/usePageTour"; import { orgSettingsTourSteps } from "@/components/common/tours/org-settings-tour"; +import { useTourSeen } from "@/hooks/useTourSeen"; const Home = () => { const orgCtx = useOrganization(); @@ -279,6 +280,15 @@ const Home = () => { }; const { startTour } = usePageTour(orgSettingsTourSteps); + const { showModal: shouldStartTour, markSeen } = useTourSeen("org-settings"); + + useEffect(() => { + if (shouldStartTour) { + markSeen(); + startTour(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldStartTour]); const config = useConfig(); diff --git a/src/components/RiskScannerDialog.tsx b/src/components/RiskScannerDialog.tsx index f9636a45..4fea1e82 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; diff --git a/src/hooks/useTourSeen.ts b/src/hooks/useTourSeen.ts new file mode 100644 index 00000000..0a29b828 --- /dev/null +++ b/src/hooks/useTourSeen.ts @@ -0,0 +1,26 @@ +"use client"; + +import { useState } from "react"; + +const storageKey = (tourKey: string) => `devguard:tourSeen:${tourKey}`; + +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 c8128b36..00a3bd07 100644 --- a/src/hooks/useWelcomeTour.ts +++ b/src/hooks/useWelcomeTour.ts @@ -1,37 +1,17 @@ -// 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 { 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); + markSeen(); }; return { From 0ee882d8155c5c9aa2687b7f7c7071b935bf8c18 Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 27 May 2026 16:46:23 +0200 Subject: [PATCH 02/14] fix: reworked copilot comments --- .../[projectSlug]/assets/[assetSlug]/page.tsx | 11 ++++++----- .../[assetSlug]/refs/[assetVersionSlug]/page.tsx | 5 ++++- .../projects/[projectSlug]/page.tsx | 5 ++++- src/components/RiskScannerDialog.tsx | 10 ++++++++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx index 057df143..a41bba30 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx @@ -154,7 +154,7 @@ const Index: FunctionComponent = () => { { icon: , name: "DevGuard CLI", - sub: "Use the devguard cli to run scans and upload the results to Devguard.", + sub: "Use the DevGuard CLI to run scans and upload the results to DevGuard.", recommended: false, githubOnly: false, slide: 16, @@ -170,7 +170,7 @@ const Index: FunctionComponent = () => { { icon: , name: "Supplier provided URL", - sub: "Provide an SBOM URLs to setup Devguard based on external data sources. This data will be periodically fetched and updated.", + sub: "Provide SBOM URLs to setup DevGuard based on external data sources. This data will be periodically fetched and updated.", recommended: false, githubOnly: false, slide: 15, @@ -186,12 +186,13 @@ const Index: FunctionComponent = () => { className={`grid gap-4 mt-4 ${cards.length === 3 ? "grid-cols-3" : "grid-cols-4"}`} > {cards.map(({ icon, name, sub, recommended, slide }) => ( -

{ @@ -216,7 +217,7 @@ const Index: FunctionComponent = () => { {sub} -
+ ))}
); 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 8fcffc33..66378b95 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 @@ -15,7 +15,7 @@ import { repoHomeTourSteps } from "@/components/common/tours/repo-home-tour"; import { useTourSeen } from "@/hooks/useTourSeen"; import "@xyflow/react/dist/style.css"; import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import type { FunctionComponent } from "react"; import { Card, @@ -143,9 +143,12 @@ const Index: FunctionComponent = () => { const { startTour } = usePageTour(repoHomeTourSteps); const { showModal: shouldStartTour, markSeen } = useTourSeen("repo-home"); + const tourStarted = useRef(false); useEffect(() => { + if (tourStarted.current) return; if (searchParams?.get("startTour") === "3" || shouldStartTour) { + tourStarted.current = true; markSeen(); startTour(); } diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx index ce50c08e..b9a15e88 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -7,7 +7,7 @@ import useRouterQuery from "@/hooks/useRouterQuery"; import { buildFilterSearchParams } from "@/utils/url"; import { debounce } from "lodash"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Form, FormProvider, useForm } from "react-hook-form"; import Markdown from "react-markdown"; import { toast } from "sonner"; @@ -140,9 +140,12 @@ export default function RepositoriesPage() { ); const { startTour } = usePageTour(tourSteps); const { showModal: shouldStartTour, markSeen } = useTourSeen("group-home"); + const tourStarted = useRef(false); useEffect(() => { + if (tourStarted.current) return; if (searchParams?.get("startTour") === "2" || shouldStartTour) { + tourStarted.current = true; markSeen(); startTour(); } diff --git a/src/components/RiskScannerDialog.tsx b/src/components/RiskScannerDialog.tsx index 4fea1e82..62dec79e 100644 --- a/src/components/RiskScannerDialog.tsx +++ b/src/components/RiskScannerDialog.tsx @@ -485,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({ From d38fb09f057e33a6c8f978330d21600127f5d95d Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Tue, 2 Jun 2026 12:30:03 +0200 Subject: [PATCH 03/14] . --- src/app/(loading-group)/[organizationSlug]/overview/page.tsx | 3 +-- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx index 21835ac1..ff3be65a 100644 --- a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx @@ -4,7 +4,6 @@ import { orgOverviewTourSteps } from "@/components/common/tours/org-overview-tou import AverageStatsSection from "@/components/organization/AverageStatsSection"; import OrganizationCompositionSection from "@/components/organization/OrganizationCompositionSection"; import TotalVulnerabilitiesSection from "@/components/organization/TotalVulnerabilitiesSection"; -import { type FunctionComponent, useEffect } from "react"; import Page from "@/components/Page"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -16,7 +15,7 @@ import { usePageTour } from "@/hooks/usePageTour"; import { useViewMode } from "@/hooks/useViewMode"; import type { OrgOverview } from "@/types/api/api"; import Link from "next/link"; -import type { FunctionComponent } from "react"; +import type { FunctionComponent, useEffect } from "react"; import useSWR from "swr"; import { useTourSeen } from "@/hooks/useTourSeen"; diff --git a/tsconfig.json b/tsconfig.json index 5505463d..56369e64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "verbatimModuleSyntax": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "paths": { "@/*": [ From 8b994cba9f2301ea3a51e0aad7bae0ad924e6e8c Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Fri, 5 Jun 2026 15:16:49 +0200 Subject: [PATCH 04/14] fix: removed unnessecary back buttons and added feat for direct click on card instead of redundant continue button --- next.config.js | 1 + .../AutomatedIntegrationSlide.tsx | 7 --- .../DevGuardCliSlide.tsx | 9 ---- .../IntegrationMethodSelectionSlide.tsx | 52 ++++--------------- .../ScannerOptionsSelectionSlide.tsx | 7 --- .../SetupInformationSourceSlide.tsx | 9 ---- 6 files changed, 12 insertions(+), 73 deletions(-) diff --git a/next.config.js b/next.config.js index 3c64ba67..e2146bf6 100644 --- a/next.config.js +++ b/next.config.js @@ -24,6 +24,7 @@ const nextConfig = { experimental: { turbopackModuleIds: "deterministic", turbopackFileSystemCacheForDev: true, + useCache: true, }, cacheComponents: true, output: "standalone", diff --git a/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/AutomatedIntegrationSlide.tsx index c5e775ff..73a7c99b 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< />
- diff --git a/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/DevGuardCliSlide.tsx index 5e6ee57d..6fdcf188 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
- diff --git a/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx index 1f1bb030..bcf8c043 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,11 +63,11 @@ const IntegrationMethodSelectionSlide: FunctionComponent< setVariant("manual")} + className="cursor-pointer mt-2" + onClick={() => { + setVariant("manual"); + api?.scrollTo(fileUploadSlideIndex); + }} > @@ -95,25 +84,6 @@ const IntegrationMethodSelectionSlide: FunctionComponent<
-
- - -
); diff --git a/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/ScannerOptionsSelectionSlide.tsx index 67c75358..de765f11 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<
- +
+ ) + ) : ( + // No repository connected yet — guide the user to connect one +
+
+ + Connect a GitLab repository for Auto Setup + + + Link this asset to a GitLab repository to use the Auto + Setup feature. + +
+ +
+ )}

@@ -160,9 +212,9 @@ const Index: FunctionComponent = () => { { icon: , name: "Manually Upload", - sub: "You already have a Scanner or a SARIF/SBOM file and want to just upload your results...", + sub: "You already have a SARIF/ SBOM file and want to scan for known vulnerabilities or manage your findings.", recommended: false, - githubOnly: true, + githubOnly: false, slide: 11, }, { From c91d8031b70b21f8a988a2fddf5cd85a4d7bbf55 Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 10 Jun 2026 11:35:43 +0200 Subject: [PATCH 06/14] fix: fixed the welcomemodal for tour seen --- .../dependency-risks/[vulnId]/page.tsx | 10 ---------- src/hooks/useTourSeen.ts | 17 +++++++++++++++++ src/hooks/useWelcomeTour.ts | 3 ++- 3 files changed, 19 insertions(+), 11 deletions(-) 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 7daead64..63775f83 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,16 +82,6 @@ import { fetcher } from "../../../../../../../../../../../data-fetcher/fetcher"; import { useActiveAssetVersion } from "../../../../../../../../../../../hooks/useActiveAssetVersion"; import useDecodedParams from "../../../../../../../../../../../hooks/useDecodedParams"; import type { ViewDependencyTreeNode } from "../../../../../../../../../../../utils/dependencyGraphHelpers"; -import MitigateDialog from "@/components/MitigateDialog"; -import { useSession } from "@/context/SessionContext"; -import AuthGuard from "@/components/AuthGuard"; -import { isMember, useCurrentUserRole } from "@/hooks/useUserRole"; -import AcceptVexRuleRecommendationDialog from "@/components/vex-rules/AcceptVexRuleRecommendationDialog"; -import VexRuleListItem from "@/components/vex-rules/VexRuleListItem"; -import { useSearchParams } from "next/navigation"; -import { useEffect } from "react"; -import { usePageTour } from "@/hooks/usePageTour"; -import { dependencyRiskTourSteps } from "@/components/common/tours/dependency-risk-tour"; import { useTourSeen } from "@/hooks/useTourSeen"; import { DocDrawer } from "@/components/common/DocDrawer"; import { convertPathsToTree } from "../../../../../../../../../../../utils/dependencyGraphHelpers"; diff --git a/src/hooks/useTourSeen.ts b/src/hooks/useTourSeen.ts index 0a29b828..40eeef94 100644 --- a/src/hooks/useTourSeen.ts +++ b/src/hooks/useTourSeen.ts @@ -2,8 +2,25 @@ 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"; diff --git a/src/hooks/useWelcomeTour.ts b/src/hooks/useWelcomeTour.ts index 00a3bd07..10a9e290 100644 --- a/src/hooks/useWelcomeTour.ts +++ b/src/hooks/useWelcomeTour.ts @@ -1,6 +1,6 @@ "use client"; -import { useTourSeen } from "./useTourSeen"; +import { dismissAllTours, useTourSeen } from "./useTourSeen"; export function useWelcomeTour() { const { showModal, markSeen } = useTourSeen("org-home"); @@ -11,6 +11,7 @@ export function useWelcomeTour() { }; const handleSkip = () => { + dismissAllTours(); markSeen(); }; From a89710f1b20f60ce2cdd74da0440e951c666340c Mon Sep 17 00:00:00 2001 From: Sebastian Kawelke Date: Mon, 15 Jun 2026 16:16:27 +0200 Subject: [PATCH 07/14] close and reset asset creation modal after successful creation Signed-off-by: Sebastian Kawelke --- .../[organizationSlug]/projects/[projectSlug]/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx index de13fa31..244e76fa 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -215,6 +215,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}`, From 0ba855ef48491c353dc547041f235f9913565b2e Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 17 Jun 2026 09:50:45 +0200 Subject: [PATCH 08/14] fix: fixed font sizes and badge --- .vscode/settings.json | 3 ++- eslint.config.mjs | 1 - package-lock.json | 6 +++--- package.json | 2 +- .../projects/[projectSlug]/assets/[assetSlug]/page.tsx | 10 ++++------ .../[organizationSlug]/projects/[projectSlug]/page.tsx | 3 --- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a1bc980c..f35c4ac3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "licenser.useSPDXLicenseFormat": true, "cSpell.words": [ "devguard" - ] + ], + "eslint.useFlatConfig": true } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 09d035dd..ff5f661a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,7 +13,6 @@ export default defineConfig([ plugins: { prettier, }, - rules: { "@next/next/no-img-element": "off", "prettier/prettier": "error", diff --git a/package-lock.json b/package-lock.json index d7977a21..62f62be6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devguard-web", - "version": "1.3.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "devguard-web", - "version": "1.3.0", + "version": "1.6.0", "hasInstallScript": true, "dependencies": { "@codemirror/lang-json": "^6.0.2", @@ -93,7 +93,7 @@ "zod": "^4.3.6" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.5", + "@eslint/eslintrc": "3.3.5", "@eslint/js": "^9.39.4", "@playwright/test": "^1.56.1", "@tailwindcss/postcss": "^4.2.2", diff --git a/package.json b/package.json index ce839450..6d2c43c8 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "zod": "^4.3.6" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.5", + "@eslint/eslintrc": "3.3.5", "@eslint/js": "^9.39.4", "@playwright/test": "^1.56.1", "@tailwindcss/postcss": "^4.2.2", diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx index 36a9993b..f9d5e4a2 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx @@ -254,17 +254,15 @@ const Index: FunctionComponent = () => { {icon} {recommended && ( Recommended )}
- - {name} - - + {name} + {sub} diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx index edfc28d6..1c6e06fc 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -10,8 +10,6 @@ import { debounce } from "lodash"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; -import Markdown from "react-markdown"; -import { toast } from "sonner"; import useSWR from "swr"; import AssetForm, { type AssetFormValues, @@ -52,7 +50,6 @@ import SubgroupsAndAssetsList, { checkType, } from "@/components/SubgroupsAndAssetsList"; import { usePageTour } from "@/hooks/usePageTour"; -import { groupHomeTourSteps } from "@/components/common/tours/group-home-tour"; import { useTourSeen } from "@/hooks/useTourSeen"; export default function RepositoriesPage() { From 416c8798fdb0558d3d2ca2be9ce6e17d3109322c Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 17 Jun 2026 10:47:48 +0200 Subject: [PATCH 09/14] fix: fixed e2e tests and card shadows --- e2e/src/pom/devguard.ts | 1 - e2e/src/pom/flows/setup.ts | 17 +--- .../[organizationSlug]/overview/page.tsx | 2 +- .../[projectSlug]/assets/[assetSlug]/page.tsx | 78 +++++++++++-------- src/components/RiskScannerDialog.tsx | 2 - .../IntegrationMethodSelectionSlide.tsx | 3 +- .../ScannerSelectionSlide.tsx | 3 +- 7 files changed, 53 insertions(+), 53 deletions(-) diff --git a/e2e/src/pom/devguard.ts b/e2e/src/pom/devguard.ts index e3d5058d..ab064195 100644 --- a/e2e/src/pom/devguard.ts +++ b/e2e/src/pom/devguard.ts @@ -121,7 +121,6 @@ export class DevGuardPOM { async setupSbomUpload() { const inputFile = path.join(__dirname, "../../assets/", "sbom.json"); await this.setup().setupOwnRiskScanning(); - await this.setup().selectManualUpload(); await this.setup().uploadSbomFile(inputFile); } diff --git a/e2e/src/pom/flows/setup.ts b/e2e/src/pom/flows/setup.ts index c9db89bd..3c0369dd 100644 --- a/e2e/src/pom/flows/setup.ts +++ b/e2e/src/pom/flows/setup.ts @@ -4,18 +4,9 @@ export class SetupFlow { constructor(private page: Page) {} async setupOwnRiskScanning() { - await this.page.getByTestId("setup-risk-scanning-button").click(); - await this.page.waitForTimeout(500); - await this.page.getByTestId("own-setup-card").click(); - await this.page.getByTestId("scanner-selection-continue").click(); - } - - async selectManualUpload() { - await this.page.waitForTimeout(500); await this.page.getByTestId("manual-upload-card").click(); - await this.page - .getByTestId("integration-method-selection-continue") - .click(); + await this.page.waitForTimeout(500); + await this.page.getByTestId("upload-manually").click(); } async uploadSbomFile(inputFile: string) { @@ -28,10 +19,8 @@ export class SetupFlow { } async setupAutoRiskScanning() { - await this.page.getByTestId("setup-risk-scanning-button").click(); await this.page.waitForTimeout(500); - await this.page.getByTestId("auto-setup-gitlab").click(); - await this.page.getByTestId("setup-method-continue").click(); + await this.page.getByTestId("gitlab-connect-repository").click(); } async createGitLabIntegration(name: string, url: string, token: string) { diff --git a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx index ff3be65a..39240486 100644 --- a/src/app/(loading-group)/[organizationSlug]/overview/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/overview/page.tsx @@ -15,7 +15,7 @@ import { usePageTour } from "@/hooks/usePageTour"; import { useViewMode } from "@/hooks/useViewMode"; import type { OrgOverview } from "@/types/api/api"; import Link from "next/link"; -import type { FunctionComponent, useEffect } from "react"; +import { useEffect, type FunctionComponent } from "react"; import useSWR from "swr"; import { useTourSeen } from "@/hooks/useTourSeen"; diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx index f9d5e4a2..f8c9acf7 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/page.tsx @@ -147,6 +147,7 @@ const Index: FunctionComponent = () => { - ))} + {cards.map( + ({ icon, name, sub, recommended, slide, testId }) => ( + + ), + )} ); })()} diff --git a/src/components/RiskScannerDialog.tsx b/src/components/RiskScannerDialog.tsx index 402d6ff8..c1381b15 100644 --- a/src/components/RiskScannerDialog.tsx +++ b/src/components/RiskScannerDialog.tsx @@ -646,10 +646,8 @@ const RiskScannerDialog: FunctionComponent = ({ prevIndex={prevIndex} /> diff --git a/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx index 6a3463fe..33b12b35 100644 --- a/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx +++ b/src/components/guides/risk-scanner-carousel-slides/IntegrationMethodSelectionSlide.tsx @@ -64,12 +64,13 @@ const IntegrationMethodSelectionSlide: FunctionComponent< { setVariant("manual"); api?.scrollTo(fileUploadSlideIndex); }} > - + Upload manually diff --git a/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx b/src/components/guides/risk-scanner-carousel-slides/ScannerSelectionSlide.tsx index 457412a6..3fe351b3 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")} > - + Date: Wed, 17 Jun 2026 11:03:28 +0200 Subject: [PATCH 10/14] fix: fixed e2e.yml --- .github/workflows/e2e.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f41dd1c0..52f8e2ea 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -78,7 +78,8 @@ jobs: if: steps.cache-playwright.outputs.cache-hit != 'true' run: | docker pull mcr.microsoft.com/playwright:v1.56.1-noble@sha256:f1e7e01021efd65dd1a2c56064be399f3e4de00fd021ac561325f2bfbb2b837a - docker save mcr.microsoft.com/playwright:v1.56.1-noble -o /tmp/playwright-image.tar + docker tag mcr.microsoft.com/playwright:v1.56.1-noble playwright:v1.56.1-noble + docker save playwright:v1.56.1-noble -o /tmp/playwright-image.tar - name: Load Playwright Docker image from cache if: steps.cache-playwright.outputs.cache-hit == 'true' @@ -93,5 +94,5 @@ jobs: -u $(id -u):$(id -g) \ -e CI=true \ -e HOME=/tmp \ - mcr.microsoft.com/playwright:v1.56.1-noble \ - bash e2e/run-devguard-tests.sh + playwright:v1.56.1-noble \ + bash e2e/run-devguard-tests.sh \ No newline at end of file From c19a45716aefb8ae82db0796e4436f0ad389c86c Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Wed, 17 Jun 2026 11:14:45 +0200 Subject: [PATCH 11/14] fix: removed e2e caching for playwright image --- .github/workflows/e2e.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 52f8e2ea..98aef98e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -67,24 +67,6 @@ jobs: OPEN_CODE_TOTP_SECRET=placeholder EOF - - name: Cache Playwright Docker image - id: cache-playwright - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 - with: - path: /tmp/playwright-image.tar - key: playwright-docker-v1.56.1-noble - - - name: Pull Playwright Docker image - if: steps.cache-playwright.outputs.cache-hit != 'true' - run: | - docker pull mcr.microsoft.com/playwright:v1.56.1-noble@sha256:f1e7e01021efd65dd1a2c56064be399f3e4de00fd021ac561325f2bfbb2b837a - docker tag mcr.microsoft.com/playwright:v1.56.1-noble playwright:v1.56.1-noble - docker save playwright:v1.56.1-noble -o /tmp/playwright-image.tar - - - name: Load Playwright Docker image from cache - if: steps.cache-playwright.outputs.cache-hit == 'true' - run: docker load -i /tmp/playwright-image.tar - - name: Run E2E tests run: | docker run --rm \ @@ -94,5 +76,5 @@ jobs: -u $(id -u):$(id -g) \ -e CI=true \ -e HOME=/tmp \ - playwright:v1.56.1-noble \ + mcr.microsoft.com/playwright:v1.56.1-noble@sha256:f1e7e01021efd65dd1a2c56064be399f3e4de00fd021ac561325f2bfbb2b837a \ bash e2e/run-devguard-tests.sh \ No newline at end of file From 0edba42bdeb6de62bb0812f5fec8371132d1553c Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Mon, 22 Jun 2026 09:31:41 +0200 Subject: [PATCH 12/14] fix: one new useAutoTour hook --- nix/npm-packages.nix | 2 +- .../[organizationSlug]/help-center/page.tsx | 8 +++--- .../[organizationSlug]/overview/page.tsx | 16 +++--------- .../[organizationSlug]/page.tsx | 2 +- .../[assetVersionSlug]/dependencies/page.tsx | 14 ++-------- .../refs/[assetVersionSlug]/page.tsx | 19 +++----------- .../assets/[assetSlug]/settings/page.tsx | 14 ++-------- .../projects/[projectSlug]/page.tsx | 19 +++----------- .../[organizationSlug]/settings/page.tsx | 14 ++-------- src/hooks/useAutoTour.ts | 26 +++++++++++++++++++ 10 files changed, 47 insertions(+), 87 deletions(-) create mode 100644 src/hooks/useAutoTour.ts diff --git a/nix/npm-packages.nix b/nix/npm-packages.nix index 934d87a3..ccb071c9 100644 --- a/nix/npm-packages.nix +++ b/nix/npm-packages.nix @@ -10,7 +10,7 @@ ../package-lock.json ]; }; - hash = "sha256-4UnvUHLe3jjpd08bdbGSpb2OpJY+2Mfnxz1lPxfZsy4="; + hash = "sha256-+fbSO7ZyRBgUoW7mAgyv7KRN7cZYKKXAycQzht+zztY="; }; node_modules = pkgs.runCommand "node-modules" { diff --git a/src/app/(loading-group)/[organizationSlug]/help-center/page.tsx b/src/app/(loading-group)/[organizationSlug]/help-center/page.tsx index fca76b28..0f1f2f04 100644 --- a/src/app/(loading-group)/[organizationSlug]/help-center/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/help-center/page.tsx @@ -149,7 +149,7 @@ export default function HelpCenterPage() { }, [firstProject?.slug, resources]); const depRiskTourHref = depRiskTarget - ? `/${activeOrg.slug}/projects/${depRiskTarget.projectSlug}/assets/${depRiskTarget.assetSlug}/refs/${depRiskTarget.refSlug}/dependency-risks/${depRiskTarget.vulnId}?startTour=4` + ? `/${activeOrg.slug}/projects/${depRiskTarget.projectSlug}/assets/${depRiskTarget.assetSlug}/refs/${depRiskTarget.refSlug}/dependency-risks/${depRiskTarget.vulnId}?startTour=dependency-risk` : undefined; const depRiskTourDisabledReason = @@ -164,7 +164,7 @@ export default function HelpCenterPage() { : undefined; const repoTourHref = repoTourTarget - ? `/${activeOrg.slug}/projects/${repoTourTarget.projectSlug}/assets/${repoTourTarget.assetSlug}?startTour=3` + ? `/${activeOrg.slug}/projects/${repoTourTarget.projectSlug}/assets/${repoTourTarget.assetSlug}?startTour=repo-home` : undefined; const repoTourDisabledReason = @@ -215,7 +215,7 @@ export default function HelpCenterPage() { Description="Get a guided overview of the organization dashboard." Button={ { const orgMenu = useOrganizationMenu(); const [mode, setMode] = useViewMode("devguard-org-view-mode"); - const { startTour } = usePageTour(orgOverviewTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("org-overview"); - - useEffect(() => { - if (shouldStartTour) { - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + useAutoTour("org-overview", orgOverviewTourSteps); const { data: orgStatistics, diff --git a/src/app/(loading-group)/[organizationSlug]/page.tsx b/src/app/(loading-group)/[organizationSlug]/page.tsx index de8a02d7..2c277cd7 100644 --- a/src/app/(loading-group)/[organizationSlug]/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/page.tsx @@ -274,7 +274,7 @@ const OrganizationHomePage: FunctionComponent = () => { const { showModal, handleStartTour, handleSkip } = useWelcomeTour(); useEffect(() => { - if (searchParams?.get("startTour") === "1") { + if (searchParams?.get("startTour") === "org-home") { startTour(); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependencies/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependencies/page.tsx index 30594dbc..aad8bb0f 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependencies/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/dependencies/page.tsx @@ -87,9 +87,8 @@ import { fetcher } from "../../../../../../../../../../data-fetcher/fetcher"; import { useActiveAsset } from "../../../../../../../../../../hooks/useActiveAsset"; import { useActiveProject } from "../../../../../../../../../../hooks/useActiveProject"; import useDecodedParams from "../../../../../../../../../../hooks/useDecodedParams"; -import { usePageTour } from "@/hooks/usePageTour"; +import { useAutoTour } from "@/hooks/useAutoTour"; import { dependencyInsightsTourSteps } from "@/components/common/tours/dependency-insights-tour"; -import { useTourSeen } from "@/hooks/useTourSeen"; const scorecardRanges: Record = { bad: [null, 3], @@ -332,16 +331,7 @@ const columnsDef: ColumnDef< const Index: FunctionComponent = () => { const assetMenu = useAssetMenu(); - const { startTour } = usePageTour(dependencyInsightsTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("dependency-insights"); - - useEffect(() => { - if (shouldStartTour) { - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + 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]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/page.tsx index 22a11df6..fa527965 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,12 +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 { useTourSeen } from "@/hooks/useTourSeen"; import "@xyflow/react/dist/style.css"; import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useMemo, useRef } from "react"; +import { useMemo } from "react"; import type { FunctionComponent } from "react"; import { Card, @@ -141,19 +140,7 @@ const Index: FunctionComponent = () => { const searchParams = useSearchParams(); const artifactName = searchParams?.get("artifact") ?? ""; - const { startTour } = usePageTour(repoHomeTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("repo-home"); - const tourStarted = useRef(false); - - useEffect(() => { - if (tourStarted.current) return; - if (searchParams?.get("startTour") === "3" || shouldStartTour) { - tourStarted.current = true; - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + 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 4031439d..866b3579 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,9 +40,8 @@ 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"; -import { useTourSeen } from "@/hooks/useTourSeen"; const firstOrUndefined = (el?: number[]): number | undefined => { if (!el) { @@ -314,16 +313,7 @@ const Index: FunctionComponent = () => { const { parentRepositoryId, parentRepositoryName } = getParentRepositoryIdAndName(project); - const { startTour } = usePageTour(repoSettingsTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("repo-settings"); - - useEffect(() => { - if (shouldStartTour) { - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + useAutoTour("repo-settings", repoSettingsTourSteps); return ( ( @@ -134,19 +133,7 @@ export default function RepositoriesPage() { () => groupHomeTourSteps(isAdmin(currentUserRole)), [currentUserRole], ); - const { startTour } = usePageTour(tourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("group-home"); - const tourStarted = useRef(false); - - useEffect(() => { - if (tourStarted.current) return; - if (searchParams?.get("startTour") === "2" || shouldStartTour) { - tourStarted.current = true; - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + useAutoTour("group-home", tourSteps); const debouncedHandleSearch = useCallback( debounce((e: React.ChangeEvent) => { diff --git a/src/app/(loading-group)/[organizationSlug]/settings/page.tsx b/src/app/(loading-group)/[organizationSlug]/settings/page.tsx index 44cc606f..0f8bd652 100644 --- a/src/app/(loading-group)/[organizationSlug]/settings/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/settings/page.tsx @@ -59,9 +59,8 @@ 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"; -import { useTourSeen } from "@/hooks/useTourSeen"; const Home = () => { const orgCtx = useOrganization(); @@ -279,16 +278,7 @@ const Home = () => { } }; - const { startTour } = usePageTour(orgSettingsTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("org-settings"); - - useEffect(() => { - if (shouldStartTour) { - markSeen(); - startTour(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldStartTour]); + useAutoTour("org-settings", orgSettingsTourSteps); const config = useConfig(); diff --git a/src/hooks/useAutoTour.ts b/src/hooks/useAutoTour.ts new file mode 100644 index 00000000..e8c0a9e9 --- /dev/null +++ b/src/hooks/useAutoTour.ts @@ -0,0 +1,26 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useEffect, useRef } from "react"; +import type { ConditionalStep } from "./usePageTour"; +import { usePageTour } from "./usePageTour"; +import { useTourSeen } from "./useTourSeen"; + +export function useAutoTour(tourKey: string, steps: ConditionalStep[]) { + const { startTour } = usePageTour(steps); + const { showModal: notSeen, markSeen } = useTourSeen(tourKey); + const searchParams = useSearchParams(); + const triggeredByParam = searchParams?.get("startTour") === tourKey; + const started = useRef(false); + + useEffect(() => { + 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 }; +} From 5bd2fd5fb04960cd9288f477e55787511ea58dec Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Mon, 22 Jun 2026 09:38:19 +0200 Subject: [PATCH 13/14] fix: dependency-risk tour --- .../dependency-risks/[vulnId]/page.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 715b58cf..86b8cdab 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 @@ -321,7 +321,8 @@ const Index: FunctionComponent = () => { const searchParams = useSearchParams(); const { startTour, registerSteps } = usePageTour(dependencyRiskTourSteps); - const { showModal: shouldStartTour, markSeen } = useTourSeen("dependency-risk"); + const { showModal: shouldStartTour, markSeen } = + useTourSeen("dependency-risk"); const [ acceptVexRuleRecommendationDialogOpen, setAcceptVexRuleRecommendationDialogOpen, @@ -380,7 +381,11 @@ const Index: FunctionComponent = () => { ); const handleGraphReady = useCallback(() => { - if (searchParams?.get("startTour") !== "4" && !shouldStartTour) return; + if ( + searchParams?.get("startTour") !== "dependency-risk" && + !shouldStartTour + ) + return; markSeen(); registerSteps(dependencyRiskTourSteps); startTour(); @@ -390,7 +395,8 @@ const Index: FunctionComponent = () => { // Path explosion: no graph is rendered so onReady never fires — start tour directly useEffect(() => { if ( - (searchParams?.get("startTour") !== "4" && !shouldStartTour) || + (searchParams?.get("startTour") !== "dependency-risk" && + !shouldStartTour) || graphLoading || (vuln?.vulnerabilityPath.length || 0) !== 0 ) From 005fb8d575f3cfe0dc9721d70582c392272557ea Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Mon, 22 Jun 2026 09:45:56 +0200 Subject: [PATCH 14/14] fix: nix sha --- nix/npm-packages.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/npm-packages.nix b/nix/npm-packages.nix index c22b4d9c..3092845a 100644 --- a/nix/npm-packages.nix +++ b/nix/npm-packages.nix @@ -10,7 +10,7 @@ ../package-lock.json ]; }; - hash = "sha256-FW3K7F/5kRiBNxNLweCtrVNJ9XpztyeLxS15IFfJ1ig="; + hash = "sha256-3DGG5Ta+DmEFO3Lb0mAu7amQbyug4qYBbo4jp20T7OQ="; }; node_modules = pkgs.runCommand "node-modules" {