Skip to content

Commit

Permalink
fix(vision): use blob urls for downloads (#6213)
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Apr 27, 2024
1 parent f3225a0 commit 8dc8d46
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 24 deletions.
37 changes: 37 additions & 0 deletions packages/@sanity/vision/src/components/DownloadCsvButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {DocumentSheetIcon} from '@sanity/icons'
import {Button, Tooltip} from '@sanity/ui'
import {type MouseEvent} from 'react'
import {useTranslation} from 'sanity'

import {visionLocaleNamespace} from '../i18n'

function preventDownload(evt: MouseEvent<HTMLButtonElement>) {
return evt.preventDefault()
}

export function DownloadCsvButton({csvUrl}: {csvUrl: string | undefined}) {
const {t} = useTranslation(visionLocaleNamespace)
const isDisabled = !csvUrl

const button = (
<Button
as="a"
disabled={isDisabled}
download={isDisabled ? undefined : 'query-result.csv'}
href={csvUrl}
icon={DocumentSheetIcon}
mode="ghost"
onClick={isDisabled ? preventDownload : undefined}
text={t('action.download-result-as-csv')}
tone="default"
/>
)

return isDisabled ? (
<Tooltip content={t('action.download-result-as-csv.not-csv-encodable')} placement="top">
{button}
</Tooltip>
) : (
button
)
}
33 changes: 9 additions & 24 deletions packages/@sanity/vision/src/components/VisionGui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,7 @@ import {
type MutationEvent,
type SanityClient,
} from '@sanity/client'
import {
CopyIcon,
DocumentSheetIcon,
ErrorOutlineIcon,
JsonIcon,
PlayIcon,
StopIcon,
} from '@sanity/icons'
import {CopyIcon, ErrorOutlineIcon, JsonIcon, PlayIcon, StopIcon} from '@sanity/icons'
import {
Box,
Button,
Expand All @@ -30,7 +23,6 @@ import {
Tooltip,
} from '@sanity/ui'
import {isHotkey} from 'is-hotkey-esm'
import {json2csv} from 'json-2-csv'
import {type ChangeEvent, createRef, PureComponent, type RefObject} from 'react'
import {type TFunction} from 'sanity'

Expand All @@ -39,13 +31,15 @@ import {VisionCodeMirror} from '../codemirror/VisionCodeMirror'
import {DEFAULT_PERSPECTIVE, isPerspective, PERSPECTIVES} from '../perspectives'
import {type VisionProps} from '../types'
import {encodeQueryString} from '../util/encodeQueryString'
import {getCsvBlobUrl, getJsonBlobUrl} from '../util/getBlobUrl'
import {getLocalStorage, type LocalStorageish} from '../util/localStorage'
import {parseApiQueryString, type ParsedApiQueryString} from '../util/parseApiQueryString'
import {prefixApiVersion} from '../util/prefixApiVersion'
import {ResizeObserver} from '../util/resizeObserver'
import {tryParseParams} from '../util/tryParseParams'
import {validateApiVersion} from '../util/validateApiVersion'
import {DelayedSpinner} from './DelayedSpinner'
import {DownloadCsvButton} from './DownloadCsvButton'
import {ParamsEditor, type ParamsEditorChangeEvent} from './ParamsEditor'
import {PerspectivePopover} from './PerspectivePopover'
import {QueryErrorDialog} from './QueryErrorDialog'
Expand Down Expand Up @@ -679,6 +673,8 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
url,
} = this.state
const hasResult = !error && !queryInProgress && typeof queryResult !== 'undefined'
const jsonUrl = hasResult ? getJsonBlobUrl(queryResult) : ''
const csvUrl = hasResult ? getCsvBlobUrl(queryResult) : ''

return (
<Root
Expand Down Expand Up @@ -980,32 +976,21 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
</TimingsTextContainer>
</TimingsCard>

{!!queryResult && (
{hasResult && (
<DownloadsCard paddingX={4} paddingY={3} sizing="border">
<DownloadsContainer gap={3} align="center">
<Text muted>{t('result.download-result-as')}</Text>
<Button
as="a"
download="query-result.json"
href={`data:application/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(queryResult, null, 2),
)}`}
href={jsonUrl}
text={t('action.download-result-as-json')}
tone="default"
mode="ghost"
icon={JsonIcon}
/>
<Button
as="a"
download="query-result.csv"
href={`data:application/csv;charset=utf-8,${encodeURIComponent(
json2csv(Array.isArray(queryResult) ? queryResult : [queryResult]),
)}`}
text={t('action.download-result-as-csv')}
tone="default"
mode="ghost"
icon={DocumentSheetIcon}
/>

<DownloadCsvButton csvUrl={csvUrl} />
</DownloadsContainer>
</DownloadsCard>
)}
Expand Down
2 changes: 2 additions & 0 deletions packages/@sanity/vision/src/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const visionLocaleStrings = defineLocalesResources('vision', {
'action.copy-url-to-clipboard': 'Copy to clipboard',
/** Label for downloading the query result as CSV */
'action.download-result-as-csv': 'CSV',
/** Tooltip text shown when the query result is not encodable as CSV */
'action.download-result-as-csv.not-csv-encodable': 'Result cannot be encoded as CSV',
/** Label for downloading the query result as JSON */
'action.download-result-as-json': 'JSON',
/** Label for stopping an ongoing listen operation */
Expand Down
42 changes: 42 additions & 0 deletions packages/@sanity/vision/src/util/getBlobUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {json2csv} from 'json-2-csv'

function getBlobUrl(content: string, mimeType: string): string {
return URL.createObjectURL(
new Blob([content], {
type: mimeType,
}),
)
}

function getMemoizedBlobUrlResolver(mimeType: string, stringEncoder: (input: any) => string) {
return (() => {
let prevResult = ''
let prevContent = ''
return (input: unknown) => {
const content = stringEncoder(input)
if (typeof content !== 'string' || content === '') {
return undefined
}

if (content === prevContent) {
return prevResult
}

prevContent = content
if (prevResult) {
URL.revokeObjectURL(prevResult)
}

prevResult = getBlobUrl(content, mimeType)
return prevResult
}
})()
}

export const getJsonBlobUrl = getMemoizedBlobUrlResolver('application/json', (input) =>
JSON.stringify(input, null, 2),
)

export const getCsvBlobUrl = getMemoizedBlobUrlResolver('text/csv', (input) => {
return json2csv(Array.isArray(input) ? input : [input]).trim()
})

0 comments on commit 8dc8d46

Please sign in to comment.