Skip to content

Commit

Permalink
Feature export filtered org domains (#4707)
Browse files Browse the repository at this point in the history
* move export btn to OrgDomains

* pass filters arg in org export query

* add new columns to headers row

* add domain filters to export query

* fix filter by tags

* export working

* fix tag separation

* filter out archived domains
  • Loading branch information
lcampbell2 authored Jul 27, 2023
1 parent a05f59d commit 6e17689
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 54 deletions.
117 changes: 97 additions & 20 deletions api/src/organization/loaders/load-organization-domain-statuses.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,112 @@
import { t } from '@lingui/macro'
import { aql } from 'arangojs'

export const loadOrganizationDomainStatuses =
({ query, userKey, i18n }) =>
async ({ orgId, blocked }) => {
({ query, userKey, i18n, language }) =>
async ({ orgId, filters }) => {
let domains
let domainFilters = aql`FILTER v.archived != true`
if (typeof filters !== 'undefined') {
filters.forEach(({ filterCategory, comparison, filterValue }) => {
if (comparison === '==') {
comparison = aql`==`
} else {
comparison = aql`!=`
}
if (filterCategory === 'dmarc-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.dmarc ${comparison} ${filterValue}
`
} else if (filterCategory === 'dkim-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.dkim ${comparison} ${filterValue}
`
} else if (filterCategory === 'https-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.https ${comparison} ${filterValue}
`
} else if (filterCategory === 'spf-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.spf ${comparison} ${filterValue}
`
} else if (filterCategory === 'ciphers-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.ciphers ${comparison} ${filterValue}
`
} else if (filterCategory === 'curves-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.curves ${comparison} ${filterValue}
`
} else if (filterCategory === 'hsts-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.hsts ${comparison} ${filterValue}
`
} else if (filterCategory === 'protocols-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.protocols ${comparison} ${filterValue}
`
} else if (filterCategory === 'certificates-status') {
domainFilters = aql`
${domainFilters}
FILTER v.status.certificates ${comparison} ${filterValue}
`
} else if (filterCategory === 'tags') {
if (filterValue === 'hidden') {
domainFilters = aql`
${domainFilters}
FILTER e.hidden ${comparison} true
`
} else if (filterValue === 'nxdomain') {
domainFilters = aql`
${domainFilters}
FILTER v.rcode ${comparison} "NXDOMAIN"
`
} else if (filterValue === 'blocked') {
domainFilters = aql`
${domainFilters}
FILTER v.blocked ${comparison} true
`
} else {
domainFilters = aql`
${domainFilters}
FILTER POSITION(claimTags, ${filterValue}) ${comparison} true
`
}
}
})
}

try {
if (blocked) {
domains = (
await query`
WITH claims, domains, organizations
FOR v, e IN 1..1 OUTBOUND ${orgId} claims
FILTER v.blocked == true
RETURN {
domain: v.domain,
status: v.status
}
`
).all()
} else {
domains = (
await query`
domains = (
await query`
WITH claims, domains, organizations
FOR v, e IN 1..1 OUTBOUND ${orgId} claims
LET claimTags = (
LET translatedTags = (
FOR tag IN e.tags || []
RETURN TRANSLATE(${language}, tag)
)
RETURN translatedTags
)[0]
${domainFilters}
RETURN {
domain: v.domain,
status: v.status
status: v.status,
tags: claimTags,
hidden: e.hidden,
rcode: v.rcode,
blocked: v.blocked,
}
`
).all()
}
).all()
} catch (err) {
console.error(`Database error occurred when user: ${userKey} running loadOrganizationDomainStatuses: ${err}`)
throw new Error(i18n._(t`Unable to load organization domain statuses. Please try again.`))
Expand Down
13 changes: 9 additions & 4 deletions api/src/organization/objects/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ export const organizationType = new GraphQLObjectType({
description:
'CSV formatted output of all domains in the organization including their email and web scan statuses.',
args: {
blocked: {
type: GraphQLBoolean,
description: 'Filters domains by blocked status.',
filters: {
type: new GraphQLList(domainFilter),
description: 'Filters used to limit domains returned.',
},
},
resolve: async (
Expand Down Expand Up @@ -119,13 +119,18 @@ export const organizationType = new GraphQLObjectType({
'spf',
'dkim',
'dmarc',
'tags',
'hidden',
'rcode',
'blocked',
]
let csvOutput = headers.join(',')
domains.forEach((domain) => {
let csvLine = `${domain.domain}`
csvLine += headers.slice(1).reduce((previousValue, currentHeader) => {
csvLine += headers.slice(1, 10).reduce((previousValue, currentHeader) => {
return `${previousValue},${domain.status[currentHeader]}`
}, '')
csvLine += `,${domain.tags.join('|')},${domain.hidden},${domain.rcode},${domain.blocked}`
csvOutput += `\n${csvLine}`
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/graphql/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ export const LANDING_PAGE_SUMMARIES = gql`
`

export const GET_ORGANIZATION_DOMAINS_STATUSES_CSV = gql`
query GetOrganizationDomainsStatusesCSV($orgSlug: Slug!) {
query GetOrganizationDomainsStatusesCSV($orgSlug: Slug!, $filters: [DomainFilter]) {
findOrganizationBySlug(orgSlug: $orgSlug) {
toCsv
toCsv(filters: $filters)
}
}
`
Expand Down
31 changes: 7 additions & 24 deletions frontend/src/organizationDetails/OrganizationDetails.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import { useLazyQuery, useQuery } from '@apollo/client'
import { useQuery } from '@apollo/client'
import { Trans } from '@lingui/macro'
import {
Box,
Expand Down Expand Up @@ -27,9 +27,8 @@ import { TieredSummaries } from '../summaries/TieredSummaries'
import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage'
import { LoadingMessage } from '../components/LoadingMessage'
import { useDocumentTitle } from '../utilities/useDocumentTitle'
import { GET_ORGANIZATION_DOMAINS_STATUSES_CSV, ORG_DETAILS_PAGE } from '../graphql/queries'
import { ORG_DETAILS_PAGE } from '../graphql/queries'
import { RadialBarChart } from '../summaries/RadialBarChart'
import { ExportButton } from '../components/ExportButton'
import { RequestOrgInviteModal } from '../organizations/RequestOrgInviteModal'
import { useUserVar } from '../utilities/userState'

Expand All @@ -48,13 +47,6 @@ export default function OrganizationDetails() {
// errorPolicy: 'ignore', // allow partial success
})

const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery(
GET_ORGANIZATION_DOMAINS_STATUSES_CSV,
{
variables: { orgSlug: orgSlug },
},
)

useEffect(() => {
if (!activeTab) {
history.replace(`/organizations/${orgSlug}/${defaultActiveTab}`)
Expand Down Expand Up @@ -160,20 +152,11 @@ export default function OrganizationDetails() {
</TabPanel>
<TabPanel>
<ErrorBoundary FallbackComponent={ErrorFallbackMessage}>
{data?.organization?.userHasPermission && (
<ExportButton
ml="auto"
my="2"
mt={{ base: '4', md: 0 }}
fileName={`${orgName}_${new Date().toLocaleDateString()}_Tracker`}
dataFunction={async () => {
const result = await getOrgDomainStatuses()
return result.data?.findOrganizationBySlug?.toCsv
}}
isLoading={orgDomainStatusesLoading}
/>
)}
<OrganizationDomains orgSlug={orgSlug} />
<OrganizationDomains
orgSlug={orgSlug}
orgName={orgName}
userHasPermission={data?.organization?.userHasPermission}
/>
</ErrorBoundary>
</TabPanel>
{!isNaN(data?.organization?.affiliations?.totalCount) && (
Expand Down
34 changes: 30 additions & 4 deletions frontend/src/organizationDetails/OrganizationDomains.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useDisclosure,
} from '@chakra-ui/react'
import { ErrorBoundary } from 'react-error-boundary'
import { number, string } from 'prop-types'
import { bool, string } from 'prop-types'

import { DomainCard } from '../domains/DomainCard'
import { ListOf } from '../components/ListOf'
Expand All @@ -23,13 +23,19 @@ import { RelayPaginationControls } from '../components/RelayPaginationControls'
import { InfoBox, InfoPanel } from '../components/InfoPanel'
import { usePaginatedCollection } from '../utilities/usePaginatedCollection'
import { useDebouncedFunction } from '../utilities/useDebouncedFunction'
import { PAGINATED_ORG_DOMAINS as FORWARD, MY_TRACKER_DOMAINS } from '../graphql/queries'
import {
PAGINATED_ORG_DOMAINS as FORWARD,
GET_ORGANIZATION_DOMAINS_STATUSES_CSV,
MY_TRACKER_DOMAINS,
} from '../graphql/queries'
import { SearchBox } from '../components/SearchBox'
import { Formik } from 'formik'
import { getRequirement, schemaToValidation } from '../utilities/fieldRequirements'
import { CheckCircleIcon, InfoIcon, WarningIcon } from '@chakra-ui/icons'
import { useLazyQuery } from '@apollo/client'
import { ExportButton } from '../components/ExportButton'

export function OrganizationDomains({ orgSlug }) {
export function OrganizationDomains({ orgSlug, orgName, userHasPermission }) {
const [orderDirection, setOrderDirection] = useState('ASC')
const [orderField, setOrderField] = useState('DOMAIN')
const [searchTerm, setSearchTerm] = useState('')
Expand Down Expand Up @@ -72,6 +78,13 @@ export function OrganizationDomains({ orgSlug }) {
nextFetchPolicy: 'cache-first',
})

const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery(
GET_ORGANIZATION_DOMAINS_STATUSES_CSV,
{
variables: { orgSlug, filters },
},
)

const { isOpen, onToggle } = useDisclosure()

if (error) return <ErrorFallbackMessage error={error} />
Expand Down Expand Up @@ -274,6 +287,19 @@ export function OrganizationDomains({ orgSlug }) {

return (
<Box>
{userHasPermission && (
<ExportButton
ml="auto"
my="2"
mt={{ base: '4', md: 0 }}
fileName={`${orgName}_${new Date().toLocaleDateString()}_Tracker`}
dataFunction={async () => {
const result = await getOrgDomainStatuses()
return result.data?.findOrganizationBySlug?.toCsv
}}
isLoading={orgDomainStatusesLoading}
/>
)}
<InfoPanel isOpen={isOpen} onToggle={onToggle}>
<InfoBox title={t`Domain`} info={t`The domain address.`} />
{/* Web statuses */}
Expand Down Expand Up @@ -401,4 +427,4 @@ export function OrganizationDomains({ orgSlug }) {
)
}

OrganizationDomains.propTypes = { domainsPerPage: number, orgSlug: string }
OrganizationDomains.propTypes = { orgSlug: string, orgName: string, userHasPermission: bool }

0 comments on commit 6e17689

Please sign in to comment.