Skip to content

Commit

Permalink
feat: Added a export csv/json button to data tables where export is a…
Browse files Browse the repository at this point in the history
…vailable (#17402)
  • Loading branch information
Gilbert09 authored Sep 14, 2023
1 parent 0ca8f92 commit c6edef3
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 21 deletions.
213 changes: 194 additions & 19 deletions frontend/src/queries/nodes/DataTable/DataTableExport.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import Papa from 'papaparse'
import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton'
import { IconExport } from 'lib/lemon-ui/icons'
import { triggerExport } from 'lib/components/ExportButton/exporter'
import { ExporterFormat } from '~/types'
import { DataNode, DataTableNode } from '~/queries/schema'
import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils'
import { isEventsQuery, isPersonsNode } from '~/queries/utils'
import { defaultDataTableColumns, extractExpressionComment } from '~/queries/nodes/DataTable/utils'
import { isEventsQuery, isHogQLQuery, isPersonsNode } from '~/queries/utils'
import { getPersonsEndpoint } from '~/queries/query'
import { ExportWithConfirmation } from '~/queries/nodes/DataTable/ExportWithConfirmation'
import { DataTableRow, dataTableLogic } from './dataTableLogic'
import { useValues } from 'kea'
import { LemonDivider, lemonToast } from '@posthog/lemon-ui'
import { asDisplay } from 'scenes/persons/person-utils'

const EXPORT_MAX_LIMIT = 10000

Expand Down Expand Up @@ -39,18 +44,148 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void
})
}

const columnDisallowList = ['person.$delete', '*']
const getCsvTableData = (dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): string[][] => {
if (isPersonsNode(query.source)) {
const filteredColumns = columns.filter((n) => !columnDisallowList.includes(n))

const csvData = dataTableRows.map((n) => {
const record = n.result as Record<string, any> | undefined
const recordWithPerson = { ...(record ?? {}), person: record?.name }

return filteredColumns.map((n) => recordWithPerson[n])
})

return [filteredColumns, ...csvData]
}

if (isEventsQuery(query.source)) {
const filteredColumns = columns
.filter((n) => !columnDisallowList.includes(n))
.map((n) => extractExpressionComment(n))

const csvData = dataTableRows.map((n) => {
return columns
.map((col, colIndex) => {
if (columnDisallowList.includes(col)) {
return null
}

if (col === 'person') {
return asDisplay(n.result?.[colIndex])
}

return n.result?.[colIndex]
})
.filter(Boolean)
})

return [filteredColumns, ...csvData]
}

if (isHogQLQuery(query.source)) {
return [columns, ...dataTableRows.map((n) => (n.result as any[]) ?? [])]
}

return []
}

const getJsonTableData = (
dataTableRows: DataTableRow[],
columns: string[],
query: DataTableNode
): Record<string, any>[] => {
if (isPersonsNode(query.source)) {
const filteredColumns = columns.filter((n) => !columnDisallowList.includes(n))

return dataTableRows.map((n) => {
const record = n.result as Record<string, any> | undefined
const recordWithPerson = { ...(record ?? {}), person: record?.name }

return filteredColumns.reduce((acc, cur) => {
acc[cur] = recordWithPerson[cur]
return acc
}, {} as Record<string, any>)
})
}

if (isEventsQuery(query.source)) {
return dataTableRows.map((n) => {
return columns.reduce((acc, col, colIndex) => {
if (columnDisallowList.includes(col)) {
return acc
}

if (col === 'person') {
acc[col] = asDisplay(n.result?.[colIndex])
return acc
}

const colName = extractExpressionComment(col)

acc[colName] = n.result?.[colIndex]

return acc
}, {} as Record<string, any>)
})
}

if (isHogQLQuery(query.source)) {
return dataTableRows.map((n) => {
const data = n.result ?? {}
return columns.reduce((acc, cur, index) => {
acc[cur] = data[index]
return acc
}, {} as Record<string, any>)
})
}

return []
}

function copyTableToCsv(dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): void {
try {
const tableData = getCsvTableData(dataTableRows, columns, query)

const csv = Papa.unparse(tableData)

navigator.clipboard.writeText(csv).then(() => {
lemonToast.success('Table copied to clipboard!')
})
} catch {
lemonToast.error('Copy failed!')
}
}

function copyTableToJson(dataTableRows: DataTableRow[], columns: string[], query: DataTableNode): void {
try {
const tableData = getJsonTableData(dataTableRows, columns, query)

const json = JSON.stringify(tableData, null, 4)

navigator.clipboard.writeText(json).then(() => {
lemonToast.success('Table copied to clipboard!')
})
} catch {
lemonToast.error('Copy failed!')
}
}

interface DataTableExportProps {
query: DataTableNode
setQuery?: (query: DataTableNode) => void
}

export function DataTableExport({ query }: DataTableExportProps): JSX.Element | null {
const { dataTableRows, columnsInResponse, columnsInQuery, queryWithDefaults } = useValues(dataTableLogic)

const source: DataNode = query.source
const filterCount =
(isEventsQuery(source) || isPersonsNode(source) ? source.properties?.length || 0 : 0) +
(isEventsQuery(source) && source.event ? 1 : 0) +
(isPersonsNode(source) && source.search ? 1 : 0)
const canExportAllColumns = isEventsQuery(source) || isPersonsNode(source)
const showExportClipboardButtons = isPersonsNode(source) || isEventsQuery(source) || isHogQLQuery(source)

return (
<LemonButtonWithDropdown
Expand All @@ -71,23 +206,63 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element |
Export current columns
</LemonButton>
</ExportWithConfirmation>,
].concat(
canExportAllColumns
? [
<ExportWithConfirmation
key={0}
placement={'bottomRight'}
onConfirm={() => startDownload(query, false)}
actor={isPersonsNode(query.source) ? 'persons' : 'events'}
limit={EXPORT_MAX_LIMIT}
>
<LemonButton fullWidth status="stealth">
Export all columns
</LemonButton>
</ExportWithConfirmation>,
]
: []
),
]
.concat(
canExportAllColumns
? [
<ExportWithConfirmation
key={0}
placement={'bottomRight'}
onConfirm={() => startDownload(query, false)}
actor={isPersonsNode(query.source) ? 'persons' : 'events'}
limit={EXPORT_MAX_LIMIT}
>
<LemonButton fullWidth status="stealth">
Export all columns
</LemonButton>
</ExportWithConfirmation>,
]
: []
)
.concat(
showExportClipboardButtons
? [
<LemonDivider key={2} />,
<LemonButton
key={3}
fullWidth
status="stealth"
onClick={() => {
if (dataTableRows) {
copyTableToCsv(
dataTableRows,
columnsInResponse ?? columnsInQuery,
queryWithDefaults
)
}
}}
>
Copy CSV to clipboard
</LemonButton>,
<LemonButton
key={3}
fullWidth
status="stealth"
onClick={() => {
if (dataTableRows) {
copyTableToJson(
dataTableRows,
columnsInResponse ?? columnsInQuery,
queryWithDefaults
)
}
}}
>
Copy JSON to clipboard
</LemonButton>,
]
: []
),
}}
type="secondary"
icon={<IconExport />}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"kea-window-values": "^3.0.0",
"md5": "^2.3.0",
"monaco-editor": "^0.39.0",
"papaparse": "^5.4.1",
"posthog-js": "1.78.5",
"posthog-js-lite": "2.0.0-alpha5",
"prettier": "^2.8.8",
Expand Down Expand Up @@ -206,6 +207,7 @@
"@types/jest-image-snapshot": "^6.1.0",
"@types/md5": "^2.3.0",
"@types/node": "^18.11.9",
"@types/papaparse": "^5.3.8",
"@types/pixelmatch": "^5.2.4",
"@types/pngjs": "^6.0.1",
"@types/query-selector-shadow-dom": "^1.0.0",
Expand Down
20 changes: 18 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c6edef3

Please sign in to comment.