Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions frontend/src/components/feedback/FeedbackCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

.feedback-card {
min-height: 220px;
padding: 32px 20px;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;

text-align: center;

background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}

.feedback-card > svg {
color: var(--accent);
}

.feedback-card--error {
border-color: #78424c;
}

.feedback-card--error > svg {
color: #fb7185;
}

.feedback-card h2 {
margin: 0;
color: var(--text-heading);
font-size: 20px;
}

.feedback-card p {
margin: 0;
color: var(--text);
font-size: 14px;
}

.feedback-spinner {
animation: feedback-spin 1s linear infinite;
}

@keyframes feedback-spin {
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: reduce) {
.feedback-spinner {
animation: none;
}
}
39 changes: 39 additions & 0 deletions frontend/src/components/feedback/FeedbackCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

import { AlertCircle, LoaderCircle, SearchX } from "lucide-react";
import "./FeedbackCard.css";

type FeedbackCardProps = {
variant: "loading" | "error" | "empty";
title: string;
message?: string;
};

function FeedbackCard({ variant, title, message }: FeedbackCardProps) {

const icons = {
loading: LoaderCircle,
error: AlertCircle,
empty: SearchX,
};

const Icon = icons[variant];

return (
<section
className={`feedback-card feedback-card--${variant}`}
role={variant === "error" ? "alert" : "status"}
>
<Icon
size={34}
aria-hidden="true"
className={variant === "loading" ? "feedback-spinner" : ""}
/>

<h2>{title}</h2>

{message && <p>{message}</p>}
</section>
);
}

export default FeedbackCard;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "lucide-react";
import type { AssociatedItem } from "../../../../types/ThreatActorTypes";
import "./ThreatActorAssociatedList.css";
import FeedbackCard from "../../../feedback/FeedbackCard";

type ThreatActorAssociatedListProps = {
type: "techniques" | "software";
Expand All @@ -30,6 +31,18 @@ function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageCh
</h2>
</div>
</div>
{/* Vis empty state dersom ingen tilknyttede entiteter finnes */}
{totalCount === 0 && (
<FeedbackCard
variant="empty"
title={
isTechnique
? "No associated techniques"
: "No associated software"
}
message="No associations are registered for this threat actor."
/>
)}

<div className="associated-list">
{items.map((item) => (
Expand Down
103 changes: 74 additions & 29 deletions frontend/src/pages/ThreatActors/ThreatActorDetailed.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ThreatActorDetailedHeader from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader";
import ThreatActorOverview from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview";
import ThreatActorDetailedInfoCards from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards";

import FeedbackCard from "../../components/feedback/FeedbackCard";
import "./ThreatActorDetailed.css";
import ThreatActorDetailedDescription from "../../components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription";
import { useParams } from "react-router-dom";
Expand Down Expand Up @@ -30,7 +30,7 @@ function ThreatActorDetailed() {
);

//Henter ut Assosierte Techniques
const {data: techniquesData} = useQuery(GET_THREAT_ACTOR_TECHNIQUES, {
const {data: techniquesData, loading: techniquesLoading, error: techniquesError} = useQuery(GET_THREAT_ACTOR_TECHNIQUES, {
variables: {
mitreId: mitreId ?? "",
limit: PAGE_SIZE,
Expand All @@ -40,34 +40,47 @@ function ThreatActorDetailed() {
});

//Henter ut Assosierte Softwares
const {data: softwaresData} = useQuery(GET_THREAT_ACTOR_SOFTWARES, {
const {data: softwaresData, loading: softwaresLoading, error: softwaresError} = useQuery(GET_THREAT_ACTOR_SOFTWARES, {
variables: {
mitreId: mitreId ?? "",
limit: PAGE_SIZE,
offset: softwaresPage * PAGE_SIZE,
},
skip: !mitreId,
});

if (!mitreId) {
return <p>Missing Threat Actor ID.</p>;
return <FeedbackCard
variant="error"
title="Missing Threat Actor ID"
/>;
}

if (detailedLoading) {
return <p>Loading Threat Actor...</p>;
return <FeedbackCard
variant="loading"
title="Loading Threat Actor..."
/>;
}

if (detailedError) {
return <p>Error loading Threat Actor: {detailedError.message}</p>;
return <FeedbackCard
variant="error"
title="Unable to load Threat Actor"
message="Something went wrong while loading the profile."
/>;
}

const actor = detailedData?.getThreatActorByMitreId;

if(!actor){
return <p>Threat Actor not found.</p>;
if (!actor) {
return <FeedbackCard
variant="empty"
title="Threat Actor not found"
message="The requested Threat Actor does not exist."
/>;
}


return (
<section className="threat-actor-detailed-page">
<div className="threat-actor-detailed-layout">
Expand Down Expand Up @@ -106,25 +119,57 @@ function ThreatActorDetailed() {
xMitreDeprecated={actor.xMitreDeprecated}
/>

<div className="threat-actor-associated">
<ThreatActorAssociatedList
type="techniques"
items={techniquesData?.getThreatActorTechniques.items ?? []}
totalCount={techniquesData?.getThreatActorTechniques.totalCount ?? 0}
page={techniquesPage}
pageSize={PAGE_SIZE}
onPageChange={setTechniquesPage}
/>

<ThreatActorAssociatedList
type="software"
items={softwaresData?.getThreatActorSoftwares.items ?? []}
totalCount={softwaresData?.getThreatActorSoftwares.totalCount ?? 0}
page={softwaresPage}
pageSize={PAGE_SIZE}
onPageChange={setSoftwaresPage}
/>
</div>

<div className="threat-actor-associated">

{/* Associated Techniques */}
{techniquesLoading ? (
<FeedbackCard
variant="loading"
title="Loading techniques..."
/>
) : techniquesError ? (
<FeedbackCard
variant="error"
title="Unable to load techniques"
message="Something went wrong while loading associated techniques."
/>
) : (
<ThreatActorAssociatedList
type="techniques"
items={techniquesData?.getThreatActorTechniques.items ?? []}
totalCount={techniquesData?.getThreatActorTechniques.totalCount ?? 0}
page={techniquesPage}
pageSize={PAGE_SIZE}
onPageChange={setTechniquesPage}
/>
)}

{/* Associated Software */}
{softwaresLoading ? (
<FeedbackCard
variant="loading"
title="Loading software..."
/>
) : softwaresError ? (
<FeedbackCard
variant="error"
title="Unable to load software"
message="Something went wrong while loading associated software."
/>
) : (
<ThreatActorAssociatedList
type="software"
items={softwaresData?.getThreatActorSoftwares.items ?? []}
totalCount={softwaresData?.getThreatActorSoftwares.totalCount ?? 0}
page={softwaresPage}
pageSize={PAGE_SIZE}
onPageChange={setSoftwaresPage}
/>
)}

</div>


</div>
</section>
Expand Down
66 changes: 51 additions & 15 deletions frontend/src/pages/ThreatActors/ThreatActors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useQuery } from "@apollo/client/react";
import { GET_ALL_THREAT_ACTOR_GRID, GET_THREAT_ACTOR_FILTER_OPTIONS } from "../../graphql/queries/threatActorQueries";
import { useState } from "react";
import ThreatActorActiveFilters from "../../components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters";
import FeedbackCard from "../../components/feedback/FeedbackCard";



Expand Down Expand Up @@ -81,7 +82,7 @@ function ThreatActor(){

const { sortBy, ...filters } = activeFilters;

const {data} = useQuery(GET_ALL_THREAT_ACTOR_GRID, {
const {data, loading, error} = useQuery(GET_ALL_THREAT_ACTOR_GRID, {
variables: {
limit: pageSize,
offset: (page - 1) * pageSize,
Expand All @@ -97,7 +98,7 @@ function ThreatActor(){
const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0;

//Henter ut data for filter
const {data: filterData} = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, {fetchPolicy: "cache-first"})
const {data: filterData, loading: filtersLoading, error: filtersError} = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, {fetchPolicy: "cache-first"})
const filterOptions = filterData?.threatActorFilterOptions;

return (
Expand All @@ -109,12 +110,27 @@ function ThreatActor(){
onSearch={handleSearch}
/>

<ThreatActorFilters
value={draftFilters}
onChange={setDraftFilters}
filterOptions={filterOptions}
onApply={handleApplyFilters}
/>
{filtersError ? (
<FeedbackCard
variant="error"
title="Unable to load filters"
message="Filter options could not be loaded. You can still search and browse threat actors."
/>
) : filtersLoading && !filterOptions ? (
<FeedbackCard
variant="loading"
title="Loading filters..."
message="Fetching available filter options."
/>
) : (
<ThreatActorFilters
value={draftFilters}
onChange={setDraftFilters}
filterOptions={filterOptions}
onApply={handleApplyFilters}
/>
)}

<ThreatActorActiveFilters
search={search}
filters={activeFilters}
Expand All @@ -123,13 +139,33 @@ function ThreatActor(){
onClearAll={handleClearAll}
/>

<ThreatActorGrid
actors={actors}
page={page}
pageSize={pageSize}
actorsCount={actorsCount}
onPageChange={setPage}
/>
{loading ? (
<FeedbackCard
variant="loading"
title="Loading threat actors..."
message="Fetching results from the catalog."
/>
) : error ? (
<FeedbackCard
variant="error"
title="Unable to load threat actors"
message="Something went wrong while loading the catalog."
/>
) : actors.length === 0 ? (
<FeedbackCard
variant="empty"
title="No matching threat actors"
message="Try changing your search or removing some filters."
/>
) : (
<ThreatActorGrid
actors={actors}
page={page}
pageSize={pageSize}
actorsCount={actorsCount}
onPageChange={setPage}
/>
)}
</section>
);
}
Expand Down