0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 21:49:51 +01:00

feat: Added a export csv/json button to data tables where export is available (#17402)

This commit is contained in:
Tom Owers 2023-09-14 17:40:33 +02:00 committed by GitHub
parent 0ca8f92069
commit c6edef3034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 21 deletions

View File

@ -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
@ -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
@ -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 />}

View File

@ -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",
@ -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",

View File

@ -1,4 +1,4 @@
lockfileVersion: '6.1'
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
@ -194,6 +194,9 @@ dependencies:
monaco-editor:
specifier: ^0.39.0
version: 0.39.0
papaparse:
specifier: ^5.4.1
version: 5.4.1
posthog-js:
specifier: 1.78.5
version: 1.78.5
@ -432,6 +435,9 @@ devDependencies:
'@types/node':
specifier: ^18.11.9
version: 18.11.9
'@types/papaparse':
specifier: ^5.3.8
version: 5.3.8
'@types/pixelmatch':
specifier: ^5.2.4
version: 5.2.4
@ -6212,6 +6218,12 @@ packages:
resolution: {integrity: sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==}
dev: false
/@types/papaparse@5.3.8:
resolution: {integrity: sha512-ArKIEOOWULbhi53wkAiRy1ze4wvrTfhpAj7Yfzva+EkmX2sV8PpFB+xqzJfzXNzK4me95FJH9QZt5NXFVGzOoQ==}
dependencies:
'@types/node': 18.11.9
dev: true
/@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: true
@ -12993,7 +13005,7 @@ packages:
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
/jsprim@2.0.2:
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
@ -14265,6 +14277,10 @@ packages:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
dev: true
/papaparse@5.4.1:
resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==}
dev: false
/param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies: