Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/jetstream/src/app/components/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { AnalyticsTrackingSetting } from './AnalyticsTrackingSetting';
import LoggerConfig from './LoggerConfig';
import { SettingsDeleteAccount } from './SettingsDeleteAccount';
import { SettingsExportImportHistory } from './SettingsExportImportHistory';
const HEIGHT_BUFFER = 170;

export const Settings = () => {
Expand Down Expand Up @@ -265,6 +266,8 @@ export const Settings = () => {
</p>
</div>

<SettingsExportImportHistory />

<div className="slds-m-top_large">
<h2 className="slds-text-heading_medium slds-m-vertical_small">Logging</h2>
<LoggerConfig />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { logger } from '@jetstream/shared/client-logger';
import { ANALYTICS_KEYS, INPUT_ACCEPT_FILETYPES, MIME_TYPES } from '@jetstream/shared/constants';
import { saveFile } from '@jetstream/shared/ui-utils';
import { InputReadFileContent } from '@jetstream/types';
import { FileSelector, fireToast, Spinner } from '@jetstream/ui';
import { useAmplitude } from '@jetstream/ui-core';
import { ClientDataImportError, exportClientHistoryData, importClientHistoryData, ImportResultSummary } from '@jetstream/ui/db';
import { useState } from 'react';

function getImportedTotal(summary: ImportResultSummary): number {
return Object.values(summary).reduce((total, count) => total + count, 0);
}

export const SettingsExportImportHistory = () => {
const { trackEvent } = useAmplitude();
const [exporting, setExporting] = useState(false);
const [importing, setImporting] = useState(false);

async function handleExport() {
try {
setExporting(true);
const envelope = await exportClientHistoryData();
saveFile(JSON.stringify(envelope, null, 2), `jetstream-history-${Date.now()}.json`, MIME_TYPES.JSON);
trackEvent(ANALYTICS_KEYS.settings_export_history);
fireToast({ message: 'Your history was exported successfully.', type: 'success' });
} catch (ex) {
logger.error('[SETTINGS] Error exporting history', ex);
fireToast({
message: 'There was a problem exporting your history. Try again or file a support ticket for assistance.',
type: 'error',
});
} finally {
setExporting(false);
}
}

async function handleImport({ content }: InputReadFileContent) {
try {
setImporting(true);
// FileSelector reads non-text extensions as an ArrayBuffer, so normalize to a string before parsing.
const text = typeof content === 'string' ? content : new TextDecoder().decode(content);
const summary = await importClientHistoryData(JSON.parse(text));
trackEvent(ANALYTICS_KEYS.settings_import_history, summary);
fireToast({
message: `Imported ${getImportedTotal(summary)} history record(s). Refresh the page to see imported Apex, API, and deployment history.`,
type: 'success',
});
} catch (ex) {
logger.error('[SETTINGS] Error importing history', ex);
// ClientDataImportError carries a user-facing explanation (e.g. "update Jetstream"); anything else
// is a parse/validation failure with a message that would not mean anything to the user.
fireToast({
message:
ex instanceof ClientDataImportError
? ex.message
: 'We could not import that file. Make sure it is an unmodified Jetstream history export and try again.',
type: 'error',
});
} finally {
setImporting(false);
}
}

return (
<div className="slds-m-top_large">
<h2 className="slds-text-heading_medium slds-m-vertical_small">Export / Import History</h2>
<p className="slds-m-bottom_small">
Export your query, load mapping, API, Apex, deployment, and recent record history to a file to back it up or move it to another
browser. Importing merges the file into your existing history without creating duplicates.
</p>
<button className="slds-button slds-button_neutral slds-is-relative" disabled={exporting} onClick={handleExport}>
{exporting && <Spinner className="slds-spinner slds-spinner_small" />}
Export History
</button>
<div className="slds-m-top_small slds-size_1-of-1 slds-medium-size_1-of-2">
<FileSelector
id="import-history-file"
label="Import History"
buttonLabel="Choose Export File"
accept={[INPUT_ACCEPT_FILETYPES.JSON]}
disabled={importing}
userHelpText="Select a previously exported Jetstream history file (.json)."
onReadFile={handleImport}
/>
{importing && <Spinner className="slds-spinner slds-spinner_small" />}
</div>
</div>
);
};

export default SettingsExportImportHistory;
6 changes: 6 additions & 0 deletions libs/shared/constants/src/lib/shared-constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
InputAcceptTypeCsv,
InputAcceptTypeExcel,
InputAcceptTypeJson,
InputAcceptTypeTsv,
InputAcceptTypeXml,
InputAcceptTypeZip,
Expand All @@ -27,12 +28,14 @@
TSV: InputAcceptTypeTsv;
EXCEL: InputAcceptTypeExcel;
XML: InputAcceptTypeXml;
JSON: InputAcceptTypeJson;
} = {
ZIP: '.zip',
CSV: '.csv',
TSV: '.tsv',
EXCEL: '.xlsx',
XML: '.xml',
JSON: '.json',

Check failure on line 38 in libs/shared/constants/src/lib/shared-constants.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-dupe-keys)

libs/shared/constants/src/lib/shared-constants.ts:38:3: Duplicate key 'JSON'
};

export const HTTP = {
Expand Down Expand Up @@ -229,6 +232,7 @@
query_HistoryTypeChanged: 'query_HistoryTypeChanged',
query_HistoryEditQueryOpened: 'query_HistoryEditQueryOpened',
query_HistoryExport: 'query_HistoryExport',
query_HistoryBackupNudgeClick: 'query_HistoryBackupNudgeClick',
query_LoadMore: 'query_LoadMore',
query_ManualQueryOpened: 'query_ManualQueryOpened',
query_ManualSoqlOpened: 'query_ManualSoqlOpened',
Expand Down Expand Up @@ -332,6 +336,8 @@
settings_password_action: 'settings_password_action',
settings_revoke_session: 'settings_revoke_session',
settings_color_scheme_changed: 'settings_color_scheme_changed',
settings_export_history: 'settings_export_history',
settings_import_history: 'settings_import_history',

/** ORGANIZATIONS */
organizations_create_modal_open: 'organizations_create_modal_open',
Expand Down
25 changes: 25 additions & 0 deletions libs/shared/ui-core/src/query/QueryHistory/QueryHistoryModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { logger } from '@jetstream/shared/client-logger';
import { ANALYTICS_KEYS } from '@jetstream/shared/constants';
import { APP_ROUTES } from '@jetstream/shared/ui-router';
import { formatNumber, useNonInitialEffect } from '@jetstream/shared/ui-utils';
import { multiWordObjectFilter } from '@jetstream/shared/utils';
import { QueryHistoryItem, QueryHistorySelection, SalesforceOrgUi, UpDown } from '@jetstream/types';
import { EmptyState, Grid, GridCol, Icon, List, Modal, SearchInput, Spinner } from '@jetstream/ui';
import { fromAppState } from '@jetstream/ui/app-state';
import { getDexieDb, queryHistoryDb } from '@jetstream/ui/db';
import { useLiveQuery } from 'dexie-react-hooks';
import { useAtomValue } from 'jotai';
import uniqBy from 'lodash/uniqBy';
import { createRef, forwardRef, useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router';
import { fromQueryHistoryState } from '../..';
import { useAmplitude } from '../../analytics';
import { QueryRestoreErrors } from '../RestoreQuery/query-restore-utils';
Expand Down Expand Up @@ -45,6 +49,9 @@ export interface QueryHistoryProps {

export const QueryHistoryModal = forwardRef<any, QueryHistoryProps>(({ className, selectedOrg, initialType, onRestore, onclose }, ref) => {
const { trackEvent } = useAmplitude();
const ability = useAtomValue(fromAppState.abilityState);
// RecordSync (history backup/sync) is a paid feature; free users keep history only in this browser.
const canSyncHistory = ability.can('access', 'RecordSync');
const [whichType, setWhichType] = useState<QueryHistoryType>(() => initialType || 'HISTORY');
const [whichOrg, setWhichOrg] = useState<WhichOrgType>('SELECTED');
const ulRef = createRef<HTMLUListElement>();
Expand Down Expand Up @@ -190,6 +197,10 @@ export const QueryHistoryModal = forwardRef<any, QueryHistoryProps>(({ className
setWhichOrg('ALL');
}

function handleBackupNudgeClick() {
trackEvent(ANALYTICS_KEYS.query_HistoryBackupNudgeClick);
}

return (
<Modal
header="Query History"
Expand All @@ -207,6 +218,20 @@ export const QueryHistoryModal = forwardRef<any, QueryHistoryProps>(({ className
onClose={() => onclose()}
>
{isRestoring && <Spinner />}
{!canSyncHistory && (
<div className="slds-text-body_small slds-text-color_weak slds-p-around_xx-small slds-m-bottom_xx-small">
<Icon
type="utility"
icon="info"
className="slds-icon slds-icon-text-default slds-icon_x-small slds-m-right_xx-small"
omitContainer
/>
Your query history is saved only in this browser.{' '}
<Link to={APP_ROUTES.BILLING.ROUTE} onClick={handleBackupNudgeClick}>
Upgrade to back it up &amp; sync across devices
</Link>
</div>
)}
{selectObjectsList.length <= 1 && <QueryHistoryEmptyState whichType={whichType} whichOrg={whichOrg} />}
{selectObjectsList.length > 1 && (
<Grid className="slds-scrollable_y">
Expand Down
2 changes: 2 additions & 0 deletions libs/shared/ui-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './lib/analysis-job-history-retention';
export * from './lib/api-request-history.db';
export * from './lib/client-data-export.db';
export * from './lib/client-data-import.db';
export * from './lib/client-data.db';
export * from './lib/query-history-object.db';
export * from './lib/query-history.db';
Expand Down
Loading
Loading