From c5788773f2c8e2873885c759a9a6d38b65e7a07a Mon Sep 17 00:00:00 2001 From: nikitita Date: Fri, 16 Oct 2026 16:38:22 +0200 Subject: [PATCH 1/2] feat: implement threat actor filter dropdowns --- .../ThreatActorFilter/ThreatActorFilter.css | 97 +++--- .../ThreatActorFilter/ThreatActorFilter.tsx | 286 ++++++++++++------ .../ThreatActorFilterDropdown.css | 209 +++++++++++++ .../ThreatActorFilterDropdown.tsx | 106 +++++++ 4 files changed, 559 insertions(+), 139 deletions(-) create mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css create mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css index 2ae09d3..1db74b1 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css @@ -1,3 +1,4 @@ + .threat-actor-filters { display: flex; flex-direction: column; @@ -6,86 +7,80 @@ .filters-title { color: var(--text-heading); - font-size: 14px; font-weight: 600; } +/* Fire kolonner på desktop */ .filters-content { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; + align-items: stretch; } -.filter-field { +/* Apply Filters fyller sin grid-celle */ +.filters-actions { + display: flex; + width: 100%; min-width: 0; - min-height: 56px; +} +.apply-filters-button { display: flex; align-items: center; - gap: 12px; + justify-content: center; + gap: 8px; + + width: 100%; + min-height: 52px; + padding: 10px 12px; - padding: 8px 14px; + background: var(--accent, #00ded4); + color: #001b1d; - background: var(--surface); + border: 1px solid var(--accent, #00ded4); + border-radius: 7px; - border: 1px solid var(--border); - border-radius: 8px; + font-family: inherit; + font-size: 12px; + font-weight: 600; + cursor: pointer; transition: - border-color 0.2s ease, - background-color 0.2s ease; + background-color 0.2s ease, + border-color 0.2s ease; } -.filter-field:hover { - border-color: #0b5558; - background: var(--surface-hover); +.apply-filters-button:hover { + background: #00bdb5; + border-color: #00bdb5; } -.filter-field:focus-within { - border-color: var(--accent); +.apply-filters-button:active { + background: #009f99; } -.filter-field > svg { - width: 21px; - height: 21px; - - flex-shrink: 0; - - color: var(--text-muted); +.apply-filters-button:focus-visible { + outline: 2px solid var(--text-heading, #ffffff); + outline-offset: 3px; } -.filter-field:focus-within > svg { - color: var(--accent); +.apply-filters-button:disabled { + opacity: 0.5; + cursor: not-allowed; } -.filter-text { - width: 100%; - min-width: 0; - - display: flex; - flex-direction: column; - gap: 2px; +/* Nettbrett */ +@media (max-width: 900px) { + .filters-content { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } -.filter-text > span { - color: var(--text-heading); - - font-size: 12px; - font-weight: 600; +/* Mobil */ +@media (max-width: 520px) { + .filters-content { + grid-template-columns: minmax(0, 1fr); + } } - -.filter-text select { - width: 100%; - - border: 0; - outline: 0; - - background: transparent; - color: var(--text-muted); - - font-size: 12px; - - cursor: pointer; -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx index 9aa7c5c..f91aabf 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx @@ -5,102 +5,212 @@ import { Globe2, MapPin, Target, + ArrowDownUp, } from "lucide-react"; - +import { useState } from "react"; +import FilterDropdown from "./ThreatActorFilterDropdown"; +import type { FilterOption } from "./ThreatActorFilterDropdown"; import "./ThreatActorFilter.css"; -function ThreatActorFilters() { +/**Mock-Data */ +const countryOptions: FilterOption[] = [ + { value: "China", label: "China", count: 58 }, + { value: "UNKNOWN", label: "Unknown", count: 56 }, + { value: "Russia", label: "Russia", count: 21 }, + { value: "Iran", label: "Iran", count: 20 }, + { value: "North Korea", label: "North Korea", count: 11 }, + { value: "India", label: "India", count: 3 }, + { value: "Lebanon", label: "Lebanon", count: 3 }, + { value: "Pakistan", label: "Pakistan", count: 3 }, +]; + +const regionOptions: FilterOption[] = [ + { value: "East Asia", label: "East Asia", count: 71 }, + { value: "UNKNOWN", label: "Unknown", count: 45 }, + { value: "Middle East", label: "Middle East", count: 31 }, + { value: "Eastern Europe", label: "Eastern Europe", count: 27 }, + { value: "South Asia", label: "South Asia", count: 8 }, + { value: "South America", label: "South America", count: 3 }, + { value: "Latin America", label: "Latin America", count: 2 }, + { value: "North America", label: "North America", count: 1 }, + { value: "Southeast Asia", label: "Southeast Asia", count: 1 }, + { value: "West Africa", label: "West Africa", count: 1 }, +]; + +const motivationOptions: FilterOption[] = [ + { value: "Espionage", label: "Espionage", count: 0 }, + { value: "Financial Gain", label: "Financial Gain", count: 0 }, + { value: "Disruption", label: "Disruption", count: 0 }, + { value: "Political / Influence", label: "Political / Influence", count: 0 }, + { value: "Mercenary", label: "Mercenary", count: 0 }, + { value: "Strategic Pre-positioning", label: "Strategic Pre-positioning", count: 0 }, + { value: "Unknown", label: "Unknown", count: 0 }, +]; + +const statusOptions: FilterOption[] = [ + { value: "ACTIVE", label: "Active", count: 107 }, + { value: "UNKNOWN", label: "Unknown", count: 75 }, + { value: "INACTIVE", label: "Inactive", count: 8 }, +]; + +const dangerOptions: FilterOption[] = [ + { value: "MODERATE", label: "Moderate", count: 90 }, + { value: "HIGH", label: "High", count: 74 }, + { value: "CRITICAL", label: "Critical", count: 20 }, + { value: "LOW", label: "Low", count: 6 }, +]; + +const sectorOptions: FilterOption[] = [ + { value: "Government", label: "Government", count: 129 }, + { value: "Technology", label: "Technology", count: 80 }, + { value: "Defense", label: "Defense", count: 61 }, + { value: "Financial Services", label: "Financial Services", count: 60 }, + { value: "Telecommunications", label: "Telecommunications", count: 42 }, + { value: "Healthcare", label: "Healthcare", count: 38 }, + { value: "Manufacturing", label: "Manufacturing", count: 31 }, + { value: "Energy", label: "Energy", count: 30 }, + { value: "Military", label: "Military", count: 26 }, + { value: "Diplomatic", label: "Diplomatic", count: 23 }, +]; + +const sortOptions: FilterOption[] = [ + { value: "name-asc", label: "Name (A - Z)" }, + { value: "name-desc", label: "Name (Z - A)" }, + { value: "mitre-asc", label: "MITRE ID (Ascending)" }, + { value: "mitre-desc", label: "MITRE ID (Descending)" }, +]; + +export type ThreatActorFilterValues = { + country: string; + region: string; + motivation: string; + status: string; + dangerLevel: string; + sector: string; + sortBy: string; +}; + +type ThreatActorFiltersProps = { + onApply: (filters: ThreatActorFilterValues) => void; +}; + +function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { + const [openFilter, setOpenFilter] = useState(null); + + + const [country, setCountry] = useState(""); + const [region, setRegion] = useState(""); + const [motivation, setMotivation] = useState(""); + const [status, setStatus] = useState(""); + const [dangerLevel, setDangerLevel] = useState(""); + const [sector, setSector] = useState(""); + const [sortBy, setSortBy] = useState("name-asc"); + + + const toggleFilter = (filter: string) => { + setOpenFilter((previous) => + previous === filter ? null : filter + ); + }; + + const handleApplyFilters = () => { + setOpenFilter(null); + + onApply({country,region,motivation,status,dangerLevel,sector,sortBy,}); +}; + return (

Filters

- - - - - - - - - - - - - + } + options={countryOptions} + value={country} + onChange={setCountry} + isOpen={openFilter === "Country"} + onToggle={() => toggleFilter("Country")} + /> + + } + options={regionOptions} + value={region} + onChange={setRegion} + isOpen={openFilter === "Region"} + onToggle={() => toggleFilter("Region")} + /> + + } + options={motivationOptions} + value={motivation} + onChange={setMotivation} + isOpen={openFilter === "Motivation"} + onToggle={() => toggleFilter("Motivation")} + /> + + } + options={statusOptions} + value={status} + onChange={setStatus} + isOpen={openFilter === "Status"} + onToggle={() => toggleFilter("Status")} + /> + + } + options={dangerOptions} + value={dangerLevel} + onChange={setDangerLevel} + isOpen={openFilter === "Danger Level"} + onToggle={() => toggleFilter("Danger Level")} + /> + + } + placeholder="All sectors" + options={sectorOptions} + value={sector} + onChange={setSector} + isOpen={openFilter === "Sector"} + onToggle={() => toggleFilter("Sector")} + /> + + } + options={sortOptions} + value={sortBy} + onChange={setSortBy} + isOpen={openFilter === "Sort by"} + onToggle={() => toggleFilter("Sort by")} + showAllOption={false} + /> + +
+ +
); diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css new file mode 100644 index 0000000..4c882f3 --- /dev/null +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css @@ -0,0 +1,209 @@ + +.filter-dropdown { + position: relative; + min-width: 0; + width: 100%; +} + +/* Selve filterboksen */ +.filter-dropdown-trigger { + display: flex; + align-items: center; + gap: 12px; + + width: 100%; + min-height: 52px; + padding: 10px 12px; + + background: var(--surface, #002629); + border: 1px solid var(--border, #0b393d); + border-radius: 7px; + + color: var(--text-heading, #e9f4f4); + font-family: inherit; + cursor: pointer; + text-align: left; + + transition: + border-color 0.2s ease, + background-color 0.2s ease; +} + +.filter-dropdown-trigger:hover { + background: var(--surface-hover, #083438); + border-color: #0b5558; +} + +.filter-dropdown-trigger[aria-expanded="true"] { + border-color: var(--accent, #00ded4); +} + +/* Ikon */ +.filter-dropdown-trigger > svg { + width: 21px; + height: 21px; + flex-shrink: 0; + color: var(--text-muted, #94a9ab); +} + +.filter-dropdown-trigger[aria-expanded="true"] > svg { + color: var(--accent, #00ded4); +} + +/* Label og valgt alternativ */ +.filter-dropdown-text { + display: flex; + flex: 1; + min-width: 0; + flex-direction: column; + gap: 3px; +} + +.filter-dropdown-text strong { + color: var(--text-heading, #e9f4f4); + font-size: 12px; + font-weight: 600; +} + +.filter-dropdown-text > span { + color: var(--text-muted, #94a9ab); + font-size: 12px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Dropdown-menyen */ +.filter-dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + z-index: 100; + + width: max(100%, 240px); + max-width: min(320px, 90vw); + max-height: 280px; + + overflow-y: auto; + overscroll-behavior: contain; + + padding: 8px; + + background: #082b2e; + border: 1px solid #31565a; + border-radius: 10px; + + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35); + + scrollbar-width: thin; + scrollbar-color: #28585b transparent; +} + +/* Scrollbar i Chrome, Safari og Edge */ +.filter-dropdown-menu::-webkit-scrollbar { + width: 6px; +} + +.filter-dropdown-menu::-webkit-scrollbar-track { + background: transparent; +} + +.filter-dropdown-menu::-webkit-scrollbar-thumb { + background: #28585b; + border-radius: 10px; +} + +.filter-dropdown-menu::-webkit-scrollbar-thumb:hover { + background: #00ded4; +} + +/* Alternativer */ +.filter-dropdown-option { + display: flex; + align-items: center; + gap: 10px; + + min-height: 36px; + padding: 9px 8px; + + border-radius: 5px; + color: #e4eeee; + font-size: 13px; + + cursor: pointer; + transition: background-color 0.15s ease; +} + +.filter-dropdown-option:hover { + background: #124348; +} + +/* Markering av valgt alternativ */ +.filter-dropdown-option:has(input:checked) { + background: #104347; +} + +/* Radio-knapper med firkantet utseende */ +.filter-dropdown-option input[type="radio"] { + appearance: none; + -webkit-appearance: none; + + width: 15px; + height: 15px; + flex-shrink: 0; + + margin: 0; + border: 1px solid #8da3a5; + border-radius: 3px; + background: #ffffff; + + cursor: pointer; + position: relative; +} + +.filter-dropdown-option input[type="radio"]:checked { + background: var(--accent, #00ded4); + border-color: var(--accent, #00ded4); +} + +.filter-dropdown-option input[type="radio"]:checked::after { + content: ""; + position: absolute; + + width: 7px; + height: 4px; + + top: 3px; + left: 3px; + + border-left: 2px solid #001b1d; + border-bottom: 2px solid #001b1d; + + transform: rotate(-45deg); +} + +/* Treffantall */ +.filter-dropdown-count { + margin-left: auto; + padding-left: 10px; + + color: #a3b8bb; + font-size: 12px; + font-variant-numeric: tabular-nums; +} + +/* Tastaturnavigasjon */ +.filter-dropdown-trigger:focus-visible, +.filter-dropdown-option:focus-within { + outline: 2px solid var(--accent, #00ded4); + outline-offset: 2px; +} + +/* Reduser animasjon ved brukerpreferanse */ +@media (prefers-reduced-motion: reduce) { + .filter-dropdown-trigger, + .filter-dropdown-option { + transition: none; + } +} diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx new file mode 100644 index 0000000..77b921d --- /dev/null +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx @@ -0,0 +1,106 @@ +import type { ReactNode } from "react"; +import "./ThreatActorFilterDropdown.css"; + +export type FilterOption = { + value: string; + label: string; + count?: number; +}; + +type FilterDropdownProps = { + label: string; + placeholder: string; + icon?: ReactNode; + options: FilterOption[]; + value: string; + onChange: (value: string) => void; + + isOpen: boolean; + onToggle: () => void; + showAllOption?: boolean; +}; + +export default function FilterDropdown({ + label, + placeholder, + icon, + options, + value, + onChange, + isOpen, + onToggle, + showAllOption = true, +}: FilterDropdownProps) { + // Sorterer filteralternativer etter antall treff. + // Alternativer uten count (f.eks. Sort by) beholder sin opprinnelige rekkefølge. + const sortedOptions = [...options].sort((a, b) => { + if (a.count === undefined || b.count === undefined) { + return 0; + } + + return b.count - a.count || a.label.localeCompare(b.label); + }); + + // Viser valgt alternativ i dropdown-knappen. + const selectedLabel =options.find((option) => option.value === value)?.label ?? placeholder; + return ( +
+ + + {isOpen && ( +
+ {/* Vises for filtre, men ikke for Sort by */} + {showAllOption && ( + + )} + + {sortedOptions.map((option) => ( + + ))} +
+ )} +
+ ); +} \ No newline at end of file From 5cd92bcf63eeb2864b275b4d86eee18dc2f30efa Mon Sep 17 00:00:00 2001 From: nikitita Date: Mon, 19 Oct 2026 12:36:42 +0200 Subject: [PATCH 2/2] feat: add dynamic threat actor filter options --- .../graphql/resolvers/threatActorResolver.ts | 20 ++-- backend/src/graphql/typeDefs.ts | 14 +++ .../src/repositories/threatActorRepository.ts | 79 ++++++++++++++- .../ThreatActorFilter/ThreatActorFilter.tsx | 97 ++++--------------- .../ThreatActorFilterDropdown.tsx | 13 +-- .../src/graphql/queries/threatActorQueries.ts | 39 +++++++- .../src/pages/ThreatActors/ThreatActors.tsx | 10 +- frontend/src/types/ThreatActorTypes.ts | 19 ++++ 8 files changed, 184 insertions(+), 107 deletions(-) diff --git a/backend/src/graphql/resolvers/threatActorResolver.ts b/backend/src/graphql/resolvers/threatActorResolver.ts index 88221fa..3286484 100644 --- a/backend/src/graphql/resolvers/threatActorResolver.ts +++ b/backend/src/graphql/resolvers/threatActorResolver.ts @@ -10,22 +10,14 @@ * * GraphQL Defs -> Resolver -> Repository -> Drizzle -> MySQL */ - - - -/** - * Resolvere for GraphQL-operasjoner relatert til ThreatActors. - * - * Resolveren mottar GraphQL-spørringer og sender arbeidet - * videre til threatActorRepository. - * - * GraphQL -> Resolver -> Repository -> Drizzle -> MySQL - */ - -import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js"; +import {getAllThreatActorsGrid, getThreatActorByMitreId, getThreatActorFilterOptions, getThreatActorSoftwares, getThreatActorTechniques} from "../../repositories/threatActorRepository.js"; export const threatActorResolvers = { Query: { + /** Henter ut alle filterOptions */ + threatActorFilterOptions: async (_parent: unknown) => { + return getThreatActorFilterOptions(); + }, /** * Henter alle ThreatActors for Grid i front end. */ @@ -48,5 +40,7 @@ export const threatActorResolvers = { getThreatActorSoftwares: async (_parent: unknown, args: {mitreId: string; limit: number; offset: number}) => { return getThreatActorSoftwares(args.mitreId, args.limit, args.offset); }, + + }, }; \ No newline at end of file diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index 0cc47e5..a55b390 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -2,6 +2,19 @@ * GraphQL schema: defines the types and operations available to the frontend. */ export const typeDefs = `#graphql + type ThreatActorFilterOption { + value: String! + label: String! + count: Int! + } + type ThreatActorFilterOptions { + countries: [ThreatActorFilterOption!]! + regions: [ThreatActorFilterOption!]! + motivations: [ThreatActorFilterOption!]! + sectors: [ThreatActorFilterOption!]! + statuses: [ThreatActorFilterOption!]! + danger_levels: [ThreatActorFilterOption!]! + } type ThreatActorGrid { name: String! mitreId: String! @@ -114,6 +127,7 @@ export const typeDefs = `#graphql getThreatActorByMitreId(mitreId: String!): ThreatActorDetailed getThreatActorTechniques(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! getThreatActorSoftwares(mitreId: String!, limit: Int!, offset: Int!): AssociatedItemsPage! + threatActorFilterOptions: ThreatActorFilterOptions! ######################################## Techinques vvvv diff --git a/backend/src/repositories/threatActorRepository.ts b/backend/src/repositories/threatActorRepository.ts index d424ad5..06c0642 100644 --- a/backend/src/repositories/threatActorRepository.ts +++ b/backend/src/repositories/threatActorRepository.ts @@ -156,7 +156,6 @@ export async function getThreatActorTechniques(mitreId: string, limit: number, o }; } - export async function getThreatActorSoftwares(mitreId: string, limit: number, offset: number) { const [softwares, countResult] = await Promise.all([ @@ -184,4 +183,82 @@ export async function getThreatActorSoftwares(mitreId: string, limit: number, of items: softwares, totalCount: countResult[0]?.totalCount ?? 0, }; +} + +// Henter ut alle filteroptions for filtreringen i ThreatActor +export async function getThreatActorFilterOptions() { + /** Henter ut land options */ + const countryFilterOptions = db + .select({country: sql`COALESCE(${threatActor.country}, 'Unknown')`, totalCount: count()}) + .from(threatActor) + .groupBy(threatActor.country); + /** Henter ut region options */ + const regionFilterOptions = db + .select({region: sql`COALESCE(${threatActor.region}, 'Unknown')`, totalCount: count()}) + .from(threatActor) + .groupBy(threatActor.region); + /** Henter ut motivasjon options */ + const motivationFilterOptions = db + .select({motivation: sql`COALESCE(${threatActor.motivation}, 'Unknown')`, totalCount: count()}) + .from(threatActor) + .groupBy(threatActor.motivation); + /** Henter ut sector options */ + const sectorFilterOptions = db + .select({sector: threatActorSector.sector, totalCount: count()}) + .from(threatActorSector) + .groupBy(threatActorSector.sector); + /** Henter ut status options*/ + const statusFilterOptions = db + .select({status: threatActor.status, totalCount: count()}) + .from(threatActor) + .groupBy(threatActor.status); + /** Henter ut trusselnivå options*/ + const dangerFilterOptions = db + .select({danger: threatActor.danger_level, totalCount: count()}) + .from(threatActor) + .groupBy(threatActor.danger_level); + //**Kjører queires parallelt, og venter til alle queries er fullført */ + const [countries, regions, motivations, sectors, statuses, danger_levels] = await Promise.all([ + countryFilterOptions, + regionFilterOptions, + motivationFilterOptions, + sectorFilterOptions, + statusFilterOptions, + dangerFilterOptions, + ]) + + //Mapper det til objekter vi enkelt kan bruke i front + return { + countries: countries.map((row) => ({ + value: row.country, + label: row.country, + count: row.totalCount + })), + regions: regions.map((row) => ({ + value: row.region, + label: row.region, + count: row.totalCount + })), + motivations: motivations.map((row) => ({ + value: row.motivation, + label: row.motivation, + count: row.totalCount + })), + sectors: sectors.map((row) => ({ + value: row.sector, + label: row.sector, + count: row.totalCount + })), + //Lager en Label for ACTIVE = A + ctive = Active + statuses: statuses.map((row) => ({ + value: row.status, + label: row.status.charAt(0).toUpperCase() + row.status.slice(1).toLowerCase(), + count: row.totalCount + })), + danger_levels: danger_levels.map((row) => ({ + value: row.danger, + label: row.danger.charAt(0).toUpperCase() + row.danger.slice(1).toLowerCase(), + count: row.totalCount + })), + }; } \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx index f91aabf..daf171f 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx +++ b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx @@ -11,67 +11,7 @@ import { useState } from "react"; import FilterDropdown from "./ThreatActorFilterDropdown"; import type { FilterOption } from "./ThreatActorFilterDropdown"; import "./ThreatActorFilter.css"; - -/**Mock-Data */ -const countryOptions: FilterOption[] = [ - { value: "China", label: "China", count: 58 }, - { value: "UNKNOWN", label: "Unknown", count: 56 }, - { value: "Russia", label: "Russia", count: 21 }, - { value: "Iran", label: "Iran", count: 20 }, - { value: "North Korea", label: "North Korea", count: 11 }, - { value: "India", label: "India", count: 3 }, - { value: "Lebanon", label: "Lebanon", count: 3 }, - { value: "Pakistan", label: "Pakistan", count: 3 }, -]; - -const regionOptions: FilterOption[] = [ - { value: "East Asia", label: "East Asia", count: 71 }, - { value: "UNKNOWN", label: "Unknown", count: 45 }, - { value: "Middle East", label: "Middle East", count: 31 }, - { value: "Eastern Europe", label: "Eastern Europe", count: 27 }, - { value: "South Asia", label: "South Asia", count: 8 }, - { value: "South America", label: "South America", count: 3 }, - { value: "Latin America", label: "Latin America", count: 2 }, - { value: "North America", label: "North America", count: 1 }, - { value: "Southeast Asia", label: "Southeast Asia", count: 1 }, - { value: "West Africa", label: "West Africa", count: 1 }, -]; - -const motivationOptions: FilterOption[] = [ - { value: "Espionage", label: "Espionage", count: 0 }, - { value: "Financial Gain", label: "Financial Gain", count: 0 }, - { value: "Disruption", label: "Disruption", count: 0 }, - { value: "Political / Influence", label: "Political / Influence", count: 0 }, - { value: "Mercenary", label: "Mercenary", count: 0 }, - { value: "Strategic Pre-positioning", label: "Strategic Pre-positioning", count: 0 }, - { value: "Unknown", label: "Unknown", count: 0 }, -]; - -const statusOptions: FilterOption[] = [ - { value: "ACTIVE", label: "Active", count: 107 }, - { value: "UNKNOWN", label: "Unknown", count: 75 }, - { value: "INACTIVE", label: "Inactive", count: 8 }, -]; - -const dangerOptions: FilterOption[] = [ - { value: "MODERATE", label: "Moderate", count: 90 }, - { value: "HIGH", label: "High", count: 74 }, - { value: "CRITICAL", label: "Critical", count: 20 }, - { value: "LOW", label: "Low", count: 6 }, -]; - -const sectorOptions: FilterOption[] = [ - { value: "Government", label: "Government", count: 129 }, - { value: "Technology", label: "Technology", count: 80 }, - { value: "Defense", label: "Defense", count: 61 }, - { value: "Financial Services", label: "Financial Services", count: 60 }, - { value: "Telecommunications", label: "Telecommunications", count: 42 }, - { value: "Healthcare", label: "Healthcare", count: 38 }, - { value: "Manufacturing", label: "Manufacturing", count: 31 }, - { value: "Energy", label: "Energy", count: 30 }, - { value: "Military", label: "Military", count: 26 }, - { value: "Diplomatic", label: "Diplomatic", count: 23 }, -]; +import type { ThreatActorFilterOptionsData } from "../../../../types/ThreatActorTypes"; const sortOptions: FilterOption[] = [ { value: "name-asc", label: "Name (A - Z)" }, @@ -91,10 +31,11 @@ export type ThreatActorFilterValues = { }; type ThreatActorFiltersProps = { + filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; onApply: (filters: ThreatActorFilterValues) => void; }; -function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { +function ThreatActorFilters({ onApply, filterOptions }: ThreatActorFiltersProps) { const [openFilter, setOpenFilter] = useState(null); @@ -128,7 +69,7 @@ function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { label="Country" placeholder="All countries" icon={} - options={countryOptions} + options={filterOptions?.countries ?? []} value={country} onChange={setCountry} isOpen={openFilter === "Country"} @@ -139,7 +80,7 @@ function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { label="Region" placeholder="All regions" icon={} - options={regionOptions} + options={filterOptions?.regions ?? []} value={region} onChange={setRegion} isOpen={openFilter === "Region"} @@ -150,18 +91,29 @@ function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { label="Motivation" placeholder="All motivations" icon={} - options={motivationOptions} + options={filterOptions?.motivations ?? []} value={motivation} onChange={setMotivation} isOpen={openFilter === "Motivation"} onToggle={() => toggleFilter("Motivation")} /> + } + placeholder="All sectors" + options={filterOptions?.sectors ?? []} + value={sector} + onChange={setSector} + isOpen={openFilter === "Sector"} + onToggle={() => toggleFilter("Sector")} + /> + } - options={statusOptions} + options={filterOptions?.statuses ?? []} value={status} onChange={setStatus} isOpen={openFilter === "Status"} @@ -172,24 +124,13 @@ function ThreatActorFilters({ onApply }: ThreatActorFiltersProps) { label="Danger Level" placeholder="All danger levels" icon={} - options={dangerOptions} + options={filterOptions?.danger_levels ?? []} value={dangerLevel} onChange={setDangerLevel} isOpen={openFilter === "Danger Level"} onToggle={() => toggleFilter("Danger Level")} /> - } - placeholder="All sectors" - options={sectorOptions} - value={sector} - onChange={setSector} - isOpen={openFilter === "Sector"} - onToggle={() => toggleFilter("Sector")} - /> - void; - isOpen: boolean; onToggle: () => void; showAllOption?: boolean; }; -export default function FilterDropdown({ - label, - placeholder, - icon, - options, - value, - onChange, - isOpen, - onToggle, - showAllOption = true, -}: FilterDropdownProps) { +export default function FilterDropdown({label, placeholder,icon,options,value,onChange,isOpen,onToggle,showAllOption = true}: FilterDropdownProps) { // Sorterer filteralternativer etter antall treff. // Alternativer uten count (f.eks. Sort by) beholder sin opprinnelige rekkefølge. const sortedOptions = [...options].sort((a, b) => { diff --git a/frontend/src/graphql/queries/threatActorQueries.ts b/frontend/src/graphql/queries/threatActorQueries.ts index 1d4859b..769c730 100644 --- a/frontend/src/graphql/queries/threatActorQueries.ts +++ b/frontend/src/graphql/queries/threatActorQueries.ts @@ -1,5 +1,5 @@ import { gql, type TypedDocumentNode } from "@apollo/client"; -import type { ThreatActorAssociatedVariables, ThreatActorDetailedData, ThreatActorDetailedVariables, ThreatActorsGridData, ThreatActorsGridVariables, ThreatActorSoftwaresData, ThreatActorTechniquesData } from "../../types/ThreatActorTypes"; +import type { ThreatActorAssociatedVariables, ThreatActorDetailedData, ThreatActorDetailedVariables, ThreatActorFilterOptionsData, ThreatActorsGridData, ThreatActorsGridVariables, ThreatActorSoftwaresData, ThreatActorTechniquesData } from "../../types/ThreatActorTypes"; //Query for ThreatActorGrid; viarbler {limit, offset} @@ -79,4 +79,41 @@ export const GET_THREAT_ACTOR_SOFTWARES: TypedDocumentNode = gql` + query GetThreatActorFilterOptions { + threatActorFilterOptions { + countries { + value + label + count + } + regions { + value + label + count + } + motivations { + value + label + count + } + sectors { + value + label + count + } + statuses { + value + label + count + } + danger_levels { + value + label + count + } + } + } `; \ No newline at end of file diff --git a/frontend/src/pages/ThreatActors/ThreatActors.tsx b/frontend/src/pages/ThreatActors/ThreatActors.tsx index 1c54030..2c95a28 100644 --- a/frontend/src/pages/ThreatActors/ThreatActors.tsx +++ b/frontend/src/pages/ThreatActors/ThreatActors.tsx @@ -4,7 +4,7 @@ import ThreatActorPageHeader from "../../components/threatactor/ThreatActorMain/ import ThreatActorSearch from "../../components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch"; import "./ThreatActors.css" import { useQuery } from "@apollo/client/react"; -import { GET_ALL_THREAT_ACTOR_GRID } from "../../graphql/queries/threatActorQueries"; +import { GET_ALL_THREAT_ACTOR_GRID, GET_THREAT_ACTOR_FILTER_OPTIONS } from "../../graphql/queries/threatActorQueries"; import { useState } from "react"; @@ -24,12 +24,18 @@ function ThreatActor(){ const actors = data?.allThreatActorsGrid.items ?? []; const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; + //Henter ut data for filter + const {data: filterData} = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, {fetchPolicy: "cache-first"}) + const filterOptions = filterData?.threatActorFilterOptions; return (
- + console.log("Selected filters:", filters)} + />