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:
parent
0ca8f92069
commit
c6edef3034
@ -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 />}
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user