diff --git a/example/Nullify Demo Dashboard.json b/example/Nullify Demo Dashboard.json index 713a951..999927f 100644 --- a/example/Nullify Demo Dashboard.json +++ b/example/Nullify Demo Dashboard.json @@ -162,6 +162,9 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" @@ -359,7 +362,8 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" - ] + ], + "ownerNamesOrQueries": [] }, "refId": "A" } @@ -548,6 +552,9 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" @@ -719,7 +726,8 @@ ], "githubRepositoryIdsOrQueries": [ "$repository" - ] + ], + "ownerNamesOrQueries": [] }, "refId": "A" } @@ -845,6 +853,9 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" @@ -1269,6 +1280,9 @@ "queryParameters": { "githubRepositoryIdsOrQueries": [ "$repository" + ], + "ownerNamesOrQueries": [ + "$owner" ] }, "refId": "A" @@ -1642,7 +1656,7 @@ "uid": "${DS_NULLIFY_DATASOURCE}" }, "definition": "Nullify Repository Query", - "description": "Filter for the repositories for which to query data", + "description": "Filter repositories for which to query data", "hide": 0, "includeAll": true, "label": "Repository", @@ -1657,6 +1671,29 @@ "skipUrlSync": false, "sort": 0, "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "nullify-grafana-datasource", + "uid": "${DS_NULLIFY_DATASOURCE}" + }, + "definition": "Nullify Owner Query", + "description": "Filter code owners for which to query data", + "hide": 0, + "includeAll": true, + "label": "Owner", + "multi": true, + "name": "owner", + "options": [], + "query": { + "queryType": "Owner" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" } ] }, @@ -1668,6 +1705,6 @@ "timezone": "", "title": "Nullify Demo Dashboard", "uid": "ecf0f5c8-bf81-479a-b72f-94b5c2855d04", - "version": 15, + "version": 3, "weekStart": "" } \ No newline at end of file diff --git a/src/api/common.ts b/src/api/common.ts new file mode 100644 index 0000000..c93c45b --- /dev/null +++ b/src/api/common.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +export const FileOwnerSchema = z.object({ + name: z.string(), + type: z.string(), +}); +export type FileOwner = z.infer; + +export const RepositorySchema = z.object({ + id: z.number(), + name: z.string(), + fullName: z.string(), + visibility: z.string(), + owner: z.object({ + id: z.number(), + name: z.string(), + slug: z.string(), + type: z.string(), + }), +}); +export type Repository = z.infer; + +export const TeamSchema = z.object({ + id: z.number(), + name: z.string(), + slug: z.string(), + privacy: z.string(), + numMembers: z.number(), +}); +export type Team = z.infer; + +export const UserSchema = z.object({ + id: z.number(), + name: z.string(), + slug: z.string(), +}); +export type User = z.infer; + +export const OrganizationSchema = z.object({ + id: z.number(), + name: z.string(), + slug: z.string(), + teams: z.array(TeamSchema), + members: z.array(UserSchema), +}); +export type Organization = z.infer; diff --git a/src/api/sastCommon.ts b/src/api/sastCommon.ts index 2d0f2fe..c7e1c3c 100644 --- a/src/api/sastCommon.ts +++ b/src/api/sastCommon.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FileOwnerSchema } from './common'; export const SastFinding = z.object({ tenantId: z.string(), @@ -17,4 +18,5 @@ export const SastFinding = z.object({ startLine: z.number(), endLine: z.number(), isAllowlisted: z.boolean(), + fileOwners: z.array(FileOwnerSchema).nullable(), }); diff --git a/src/api/sastEvents.ts b/src/api/sastEvents.ts index ffcd240..b4e3607 100644 --- a/src/api/sastEvents.ts +++ b/src/api/sastEvents.ts @@ -3,7 +3,7 @@ import { DataFrame, FieldType, TimeRange, createDataFrame } from '@grafana/data' import { FetchResponse, isFetchError } from '@grafana/runtime'; import { SastEventType, SastEventsQueryOptions } from 'types'; import { SastFinding } from './sastCommon'; -import { unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; const MAX_API_REQUESTS = 10; @@ -259,6 +259,7 @@ type SastEventsEvent = z.infer; interface SastEventsApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; branch?: string; eventType?: string[]; fromTime?: string; // ISO string @@ -283,6 +284,11 @@ export const processSastEvents = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.branch ? { branch: queryOptions.queryParameters.branch } : {}), ...(queryOptions.queryParameters.eventTypes && queryOptions.queryParameters.eventTypes.length > 0 ? { eventType: queryOptions.queryParameters.eventTypes } diff --git a/src/api/sastSummary.ts b/src/api/sastSummary.ts index 9187d73..3bb176a 100644 --- a/src/api/sastSummary.ts +++ b/src/api/sastSummary.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { DataFrame, FieldType, createDataFrame } from '@grafana/data'; import { FetchResponse } from '@grafana/runtime'; import { SastSummaryQueryOptions } from 'types'; -import { prepend_severity_idx, unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { prepend_severity_idx, unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; import { SastFinding } from './sastCommon'; const SastSummaryApiResponseSchema = z.object({ @@ -12,6 +12,7 @@ const SastSummaryApiResponseSchema = z.object({ interface SastSummaryApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; severity?: string; } @@ -27,6 +28,11 @@ export const processSastSummary = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.severity ? { severity: queryOptions.queryParameters.severity } : {}), }; const endpointPath = 'sast/summary'; @@ -85,10 +91,20 @@ export const processSastSummary = async ( values: parseResult.data.vulnerabilities.map((vuln) => vuln.isAllowlisted), }, { - name: 'repositoryName', + name: 'repository', type: FieldType.string, values: parseResult.data.vulnerabilities.map((vuln) => vuln.repository), }, + { + name: 'branch', + type: FieldType.string, + values: parseResult.data.vulnerabilities.map((vuln) => vuln.branch), + }, + { + name: 'owners', + type: FieldType.string, + values: parseResult.data.vulnerabilities.map((vuln) => vuln.fileOwners?.map((owner) => owner.name).join(', ')), + }, ], }); }; diff --git a/src/api/scaCommon.ts b/src/api/scaCommon.ts index 3d4c727..7df85ea 100644 --- a/src/api/scaCommon.ts +++ b/src/api/scaCommon.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FileOwnerSchema } from './common'; const ScaEventsCve = z.object({ id: z.string().optional(), @@ -30,6 +31,8 @@ export const ScaEventsDependencyFinding = z.object({ packageFilePath: z.string().optional(), version: z.string().optional(), filePath: z.string().optional(), + repository: z.string().optional(), + branch: z.string().optional(), line: z.number().optional(), numCritical: z.number().default(0), numHigh: z.number().default(0), @@ -37,4 +40,5 @@ export const ScaEventsDependencyFinding = z.object({ numLow: z.number().default(0), numUnknown: z.number().default(0), vulnerabilities: z.array(ScaEventsVulnerability).optional().nullable(), + fileOwners: z.array(FileOwnerSchema).nullable(), }); diff --git a/src/api/scaEvents.ts b/src/api/scaEvents.ts index 1324131..41e1912 100644 --- a/src/api/scaEvents.ts +++ b/src/api/scaEvents.ts @@ -3,7 +3,7 @@ import { DataFrame, FieldType, TimeRange, createDataFrame } from '@grafana/data' import { FetchResponse } from '@grafana/runtime'; import { ScaEventType, ScaEventsQueryOptions } from 'types'; import { ScaEventsDependencyFinding } from './scaCommon'; -import { unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; const MAX_API_REQUESTS = 10; @@ -208,6 +208,7 @@ type ScaEventsEvent = z.infer; interface ScaEventsApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; branch?: string; eventType?: string[]; fromTime?: string; // ISO string @@ -232,6 +233,11 @@ export const processScaEvents = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.branch ? { branch: queryOptions.queryParameters.branch } : {}), ...(queryOptions.queryParameters.eventTypes && queryOptions.queryParameters.eventTypes.length > 0 ? { eventType: queryOptions.queryParameters.eventTypes } diff --git a/src/api/scaSummary.ts b/src/api/scaSummary.ts index 9fa7e72..cec730c 100644 --- a/src/api/scaSummary.ts +++ b/src/api/scaSummary.ts @@ -3,7 +3,7 @@ import { DataFrame, FieldType, createDataFrame } from '@grafana/data'; import { FetchResponse } from '@grafana/runtime'; import { ScaSummaryQueryOptions } from 'types'; import { ScaEventsDependencyFinding } from './scaCommon'; -import { unwrapRepositoryTemplateVariables } from 'utils/utils'; +import { unwrapOwnerTemplateVariables, unwrapRepositoryTemplateVariables } from 'utils/utils'; const ScaSummaryApiResponseSchema = z.object({ vulnerabilities: z.array(ScaEventsDependencyFinding).nullable(), @@ -12,6 +12,7 @@ const ScaSummaryApiResponseSchema = z.object({ interface ScaSummaryApiRequest { githubRepositoryId?: number[]; + fileOwnerName?: string[]; severity?: string; } @@ -28,6 +29,9 @@ export interface UnwoundScaEventsDependencyFinding { numMedium: number | undefined; numLow: number | undefined; numUnknown: number | undefined; + repository: string | undefined; + branch: string | undefined; + owners: string[] | undefined; vulnerabilityHasFix: boolean | undefined; vulnerabilityTitle: string | undefined; vulnerabilitySeverity: string | undefined; @@ -50,6 +54,11 @@ export const processScaSummary = async ( ), } : {}), + ...(queryOptions.queryParameters.ownerNamesOrQueries + ? { + fileOwnerName: unwrapOwnerTemplateVariables(queryOptions.queryParameters.ownerNamesOrQueries), + } + : {}), ...(queryOptions.queryParameters.package ? { package: queryOptions.queryParameters.package } : {}), }; const endpointPath = 'sca/summary'; @@ -69,7 +78,7 @@ export const processScaSummary = async ( }; } - const unwoundFindings = parseResult.data.vulnerabilities?.flatMap(finding => { + const unwoundFindings = parseResult.data.vulnerabilities?.flatMap((finding) => { let result: UnwoundScaEventsDependencyFinding[] = []; for (const vuln of finding.vulnerabilities ?? []) { result.push({ @@ -85,6 +94,9 @@ export const processScaSummary = async ( numMedium: finding.numMedium, numLow: finding.numLow, numUnknown: finding.numUnknown, + repository: finding.repository, + branch: finding.branch, + owners: finding.fileOwners?.map((owner) => owner.name), vulnerabilityHasFix: vuln.hasFix, vulnerabilityTitle: vuln.title, vulnerabilitySeverity: vuln.severity, @@ -95,7 +107,7 @@ export const processScaSummary = async ( vulnerabilityVersion: vuln.version, }); } - return result + return result; }); return createDataFrame({ @@ -151,6 +163,21 @@ export const processScaSummary = async ( type: FieldType.number, values: unwoundFindings?.map((vuln) => vuln.numLow), }, + { + name: 'repository', + type: FieldType.string, + values: unwoundFindings?.map((vuln) => vuln.repository), + }, + { + name: 'branch', + type: FieldType.string, + values: unwoundFindings?.map((vuln) => vuln.branch), + }, + { + name: 'owners', + type: FieldType.string, + values: unwoundFindings?.map((vuln) => vuln.owners?.join(', ')), + }, { name: 'vulnerabilityHasFix', type: FieldType.boolean, diff --git a/src/components/Fields/OwnersField.tsx b/src/components/Fields/OwnersField.tsx new file mode 100644 index 0000000..f6d2a9c --- /dev/null +++ b/src/components/Fields/OwnersField.tsx @@ -0,0 +1,96 @@ +import { SelectableValue, TypedVariableModel } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import { MultiSelect } from '@grafana/ui'; +import { Organization } from 'api/common'; +import React, { useEffect, useRef, useState } from 'react'; +import { NullifyVariableQuery, NullifyVariableQueryType, OwnerEntity, OwnerEntityType } from 'types'; + +export interface OwnerFieldProps { + getOwnerEntities: () => Promise; + selectedOwnerNamesOrQueries: string[]; + setSelectedOwnerNamesOrQueries: (selectedOwners: string[]) => void; +} + +export const OwnerField = (props: OwnerFieldProps) => { + const [selectedValues, setSelectedValues] = useState>>( + props.selectedOwnerNamesOrQueries.map((selected) => ({ label: selected.toString(), value: selected })) + ); + const [customOptions, setCustomOptions] = useState>>([]); + const [allOwnerOptions, setAllOwnerOptions] = useState> | undefined>(undefined); + + const getOwnerEntitiesRef = useRef(props.getOwnerEntities); + const selectedOwnerNamesOrQueriesRef = useRef(props.selectedOwnerNamesOrQueries); + + const onSelectedValuesChange = (newSelectedValues: Array>) => { + setSelectedValues(newSelectedValues); + const selectedOwners = newSelectedValues + .map((selectable) => selectable.value) + .filter((ownerNameOrQuery): ownerNameOrQuery is string => ownerNameOrQuery !== undefined); + props.setSelectedOwnerNamesOrQueries(selectedOwners); + }; + + useEffect(() => { + const formatOwnerSelectableValue = (owner: OwnerEntity): SelectableValue => { + return { label: `${owner.name} (${owner.type})`, value: owner.name }; + }; + const formatVariableSelectableValue = (variableName: string): SelectableValue => { + return { label: `$${variableName}`, value: `$${variableName}` }; + }; + + const variables: TypedVariableModel[] = getTemplateSrv() + .getVariables() + .filter( + (variable) => + variable.type === 'query' && + (variable.query as NullifyVariableQuery).queryType === NullifyVariableQueryType.Owner + ); + + getOwnerEntitiesRef + .current() + .then((ownerEntities) => { + if (ownerEntities) { + setAllOwnerOptions([ + ...variables.map((variable) => formatVariableSelectableValue(variable.id)), + ...ownerEntities.map(formatOwnerSelectableValue), + ]); + + setSelectedValues([ + ...variables + .map((variable) => formatVariableSelectableValue(variable.id)) + .filter((selectableValue) => + selectedOwnerNamesOrQueriesRef.current.includes(selectableValue.value || '') + ), + ...ownerEntities + .map(formatOwnerSelectableValue) + .filter((selectableValue) => + selectedOwnerNamesOrQueriesRef.current.includes(selectableValue.value || '') + ), + ]); + } else { + setAllOwnerOptions([]); + } + }) + .catch(() => { + setAllOwnerOptions([]); + }); + }, []); + + return ( + <> + { + setCustomOptions([...customOptions, { value: optionValue, label: optionValue }]); + onSelectedValuesChange([...selectedValues, { value: optionValue, label: optionValue }]); + }} + /> + + ); +}; diff --git a/src/components/Fields/RepositoryField.tsx b/src/components/Fields/RepositoryField.tsx index 849b609..240902f 100644 --- a/src/components/Fields/RepositoryField.tsx +++ b/src/components/Fields/RepositoryField.tsx @@ -1,12 +1,9 @@ import { SelectableValue, TypedVariableModel } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { MultiSelect } from '@grafana/ui'; +import { Repository } from 'api/common'; import React, { useEffect, useRef, useState } from 'react'; - -export interface Repository { - id: string; - name: string; -} +import { NullifyVariableQuery, NullifyVariableQueryType } from 'types'; export interface RepositoryFieldProps { getRepositories: () => Promise; @@ -36,13 +33,19 @@ export const RepositoryField = (props: RepositoryFieldProps) => { useEffect(() => { const formatRepositorySelectableValue = (repo: Repository): SelectableValue => { - return { label: `${repo.name} (${repo.id})`, value: Number(repo.id) }; + return { label: `${repo.name} (${repo.id})`, value: repo.id }; }; const formatVariableSelectableValue = (variableName: string): SelectableValue => { return { label: `$${variableName}`, value: `$${variableName}` }; }; - const variables: TypedVariableModel[] = getTemplateSrv().getVariables(); + const variables: TypedVariableModel[] = getTemplateSrv() + .getVariables() + .filter( + (variable) => + variable.type === 'query' && + (variable.query as NullifyVariableQuery).queryType === NullifyVariableQueryType.Repository + ); getRepositoriesRef .current() @@ -80,12 +83,12 @@ export const RepositoryField = (props: RepositoryFieldProps) => { isLoading={allRepositoryOptions === undefined} closeMenuOnSelect={false} placeholder="Select repositories or enter a repository ID" - noOptionsMessage="No repositories found. To query a repository not listed, enter the repository ID." + noOptionsMessage="No repositories found. To query a repository not listed, enter the repository ID number." allowCustomValue value={selectedValues} onChange={onSelectedValuesChange} isValidNewOption={(inputValue) => { - return !isNaN(Number(inputValue)); + return !isNaN(Number(inputValue)) && Number(inputValue) !== 0; }} onCreateOption={(optionValue) => { const optionNumber = Number(optionValue); diff --git a/src/components/Query/SastEventsSubquery.tsx b/src/components/Query/SastEventsSubquery.tsx index c3fb583..a7ceec5 100644 --- a/src/components/Query/SastEventsSubquery.tsx +++ b/src/components/Query/SastEventsSubquery.tsx @@ -4,6 +4,7 @@ import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { NullifyDataSource } from '../../datasource'; import { NullifyDataSourceOptions, NullifyQueryOptions, SastEventTypeDescriptions } from '../../types'; import { RepositoryField } from 'components/Fields/RepositoryField'; +import { OwnerField } from 'components/Fields/OwnersField'; type Props = QueryEditorProps; @@ -13,9 +14,14 @@ const SastEventTypeOptions: Array> = Object.entries(Sast export function SastEventsSubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'sast/events' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'sast/events' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -25,6 +31,16 @@ export function SastEventsSubquery(props: Props) { onRunQuery(); }; + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'sast/events', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; + const onBranchChange = (event: ChangeEvent) => { onChange({ ...query, @@ -46,6 +62,16 @@ export function SastEventsSubquery(props: Props) { setSelectedRepositoryIdsOrQueries={onRepositoriesChange} /> + + + ; @@ -17,9 +18,14 @@ const severity_options: Array> = [ export function SastSummarySubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'sast/summary' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'sast/summary' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -29,6 +35,16 @@ export function SastSummarySubquery(props: Props) { onRunQuery(); }; + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'sast/summary', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; + const onSeverityChange = (new_severity: string) => { onChange({ ...query, @@ -50,6 +66,16 @@ export function SastSummarySubquery(props: Props) { setSelectedRepositoryIdsOrQueries={onRepositoriesChange} /> + + + ; @@ -13,9 +14,14 @@ const ScaEventTypeOptions: Array> = Object.entries(ScaEv export function ScaEventsSubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'sca/events' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'sca/events' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -25,6 +31,16 @@ export function ScaEventsSubquery(props: Props) { onRunQuery(); }; + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'sca/events', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; + const onBranchChange = (event: ChangeEvent) => { onChange({ ...query, @@ -46,6 +62,16 @@ export function ScaEventsSubquery(props: Props) { setSelectedRepositoryIdsOrQueries={onRepositoriesChange} /> + + + ; export function ScaSummarySubquery(props: Props) { const { query, onChange, onRunQuery } = props; - const [selectedRepositories, setSelectedRepositories] = useState>(query.queryParameters?.githubRepositoryIdsOrQueries || []); + const [selectedRepositories, setSelectedRepositories] = useState>( + query.endpoint === 'sca/summary' ? query.queryParameters?.githubRepositoryIdsOrQueries || [] : [] + ); + const [selectedOwners, setSelectedOwners] = useState( + query.endpoint === 'sca/summary' ? query.queryParameters?.ownerNamesOrQueries || [] : [] + ); - const onRepositoriesChange = (repositories: Array<(number | string)>) => { + const onRepositoriesChange = (repositories: Array) => { setSelectedRepositories(repositories); onChange({ ...query, @@ -21,6 +27,16 @@ export function ScaSummarySubquery(props: Props) { onRunQuery(); }; + const onOwnersChange = (owners: string[]) => { + setSelectedOwners(owners); + onChange({ + ...query, + endpoint: 'sca/summary', + queryParameters: { ...query.queryParameters, ownerNamesOrQueries: owners }, + }); + onRunQuery(); + }; + const onPackageChange = (event: ChangeEvent) => { onChange({ ...query, @@ -41,6 +57,16 @@ export function ScaSummarySubquery(props: Props) { setSelectedRepositoryIdsOrQueries={onRepositoriesChange} /> + + + { - useEffect(() => { - onChange({ queryType: VariableQueryType.Repository }, `Nullify ${VariableQueryType.Repository} Query`); - }, [onChange]); + const queryTypeToSelectableValue = ( + queryType: NullifyVariableQueryType + ): SelectableValue => { + return { label: queryType, value: queryType }; + }; + + const variableOptions: Array> = + Object.values(NullifyVariableQueryType).map(queryTypeToSelectableValue); + + const [variableSelection, setVariableSelection] = useState | null>( + queryTypeToSelectableValue(query.queryType) + ); return ( <> - This query variable enables the creation of a dashboard-wide filter for repositories. - {/* ADD SELECTOR FOR OTHER QUERY TYPES (e.g. Repo/Branch/Team) */} + +