Add ability to exclude environments from data export

This commit is contained in:
Gregory Schier
2025-01-11 11:36:00 -08:00
parent 88b410bf99
commit 8dff75ad4f
10 changed files with 59 additions and 41 deletions

View File

@@ -1002,8 +1002,11 @@ async fn cmd_export_data(
window: WebviewWindow, window: WebviewWindow,
export_path: &str, export_path: &str,
workspace_ids: Vec<&str>, workspace_ids: Vec<&str>,
include_environments: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let export_data = get_workspace_export_resources(window.app_handle(), workspace_ids).await; let export_data = get_workspace_export_resources(&window, workspace_ids, include_environments)
.await
.map_err(|e| e.to_string())?;
let f = File::options() let f = File::options()
.create(true) .create(true)
.truncate(true) .truncate(true)

View File

@@ -2175,7 +2175,8 @@ pub async fn batch_upsert<R: Runtime>(
pub async fn get_workspace_export_resources<R: Runtime>( pub async fn get_workspace_export_resources<R: Runtime>(
mgr: &impl Manager<R>, mgr: &impl Manager<R>,
workspace_ids: Vec<&str>, workspace_ids: Vec<&str>,
) -> WorkspaceExport { include_environments: bool,
) -> Result<WorkspaceExport> {
let mut data = WorkspaceExport { let mut data = WorkspaceExport {
yaak_version: mgr.package_info().version.clone().to_string(), yaak_version: mgr.package_info().version.clone().to_string(),
yaak_schema: 2, yaak_schema: 2,
@@ -2190,24 +2191,19 @@ pub async fn get_workspace_export_resources<R: Runtime>(
}; };
for workspace_id in workspace_ids { for workspace_id in workspace_ids {
data.resources data.resources.workspaces.push(get_workspace(mgr, workspace_id).await?);
.workspaces data.resources.environments.append(&mut list_environments(mgr, workspace_id).await?);
.push(get_workspace(mgr, workspace_id).await.expect("Failed to get workspace")); data.resources.folders.append(&mut list_folders(mgr, workspace_id).await?);
data.resources.environments.append( data.resources.http_requests.append(&mut list_http_requests(mgr, workspace_id).await?);
&mut list_environments(mgr, workspace_id).await.expect("Failed to get environments"), data.resources.grpc_requests.append(&mut list_grpc_requests(mgr, workspace_id).await?);
);
data.resources
.folders
.append(&mut list_folders(mgr, workspace_id).await.expect("Failed to get folders"));
data.resources.http_requests.append(
&mut list_http_requests(mgr, workspace_id).await.expect("Failed to get http requests"),
);
data.resources.grpc_requests.append(
&mut list_grpc_requests(mgr, workspace_id).await.expect("Failed to get grpc requests"),
);
} }
data // Nuke environments if we don't want them
if !include_environments {
data.resources.environments.clear();
}
Ok(data)
} }
// Generate the created_at or updated_at timestamps for an upsert operation, depending on the ID // Generate the created_at or updated_at timestamps for an upsert operation, depending on the ID

View File

@@ -13,9 +13,6 @@ pub enum Error {
#[error("Unknown model: {0}")] #[error("Unknown model: {0}")]
UnknownModel(String), UnknownModel(String),
#[error("Workspace not configured for sync: {0}")]
WorkspaceSyncNotConfigured(String),
#[error("I/o error: {0}")] #[error("I/o error: {0}")]
IoError(#[from] io::Error), IoError(#[from] io::Error),
@@ -24,7 +21,7 @@ pub enum Error {
#[error("Invalid sync file: {0}")] #[error("Invalid sync file: {0}")]
InvalidSyncFile(String), InvalidSyncFile(String),
#[error("Watch error: {0}")] #[error("Watch error: {0}")]
NotifyError(#[from] notify::Error), NotifyError(#[from] notify::Error),
} }

View File

@@ -111,7 +111,7 @@ pub(crate) async fn get_db_candidates<R: Runtime>(
sync_dir: &Path, sync_dir: &Path,
) -> Result<Vec<DbCandidate>> { ) -> Result<Vec<DbCandidate>> {
let models: HashMap<_, _> = let models: HashMap<_, _> =
workspace_models(mgr, workspace_id).await.into_iter().map(|m| (m.id(), m)).collect(); workspace_models(mgr, workspace_id).await?.into_iter().map(|m| (m.id(), m)).collect();
let sync_states: HashMap<_, _> = list_sync_states_for_workspace(mgr, workspace_id, sync_dir) let sync_states: HashMap<_, _> = list_sync_states_for_workspace(mgr, workspace_id, sync_dir)
.await? .await?
.into_iter() .into_iter()
@@ -270,12 +270,15 @@ pub(crate) fn compute_sync_ops(
.collect() .collect()
} }
async fn workspace_models<R: Runtime>(mgr: &impl Manager<R>, workspace_id: &str) -> Vec<SyncModel> { async fn workspace_models<R: Runtime>(
let resources = get_workspace_export_resources(mgr, vec![workspace_id]).await.resources; mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<SyncModel>> {
let resources = get_workspace_export_resources(mgr, vec![workspace_id], true).await?.resources;
let workspace = resources.workspaces.iter().find(|w| w.id == workspace_id); let workspace = resources.workspaces.iter().find(|w| w.id == workspace_id);
let workspace = match workspace { let workspace = match workspace {
None => return Vec::new(), None => return Ok(Vec::new()),
Some(w) => w, Some(w) => w,
}; };
@@ -294,7 +297,7 @@ async fn workspace_models<R: Runtime>(mgr: &impl Manager<R>, workspace_id: &str)
sync_models.push(SyncModel::GrpcRequest(m)); sync_models.push(SyncModel::GrpcRequest(m));
} }
sync_models Ok(sync_models)
} }
pub(crate) async fn apply_sync_ops<R: Runtime>( pub(crate) async fn apply_sync_ops<R: Runtime>(

View File

@@ -420,7 +420,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
<div className="h-full px-1.5 overflow-y-auto pt-2 pb-1"> <div className="h-full px-1.5 overflow-y-auto pt-2 pb-1">
{filteredGroups.map((g) => ( {filteredGroups.map((g) => (
<div key={g.key} className="mb-1.5 w-full"> <div key={g.key} className="mb-1.5 w-full">
<Heading size={2} className="!text-xs uppercase px-1.5 h-sm flex items-center"> <Heading level={2} className="!text-xs uppercase px-1.5 h-sm flex items-center">
{g.label} {g.label}
</Heading> </Heading>
{g.items.map((v) => ( {g.items.map((v) => (

View File

@@ -6,6 +6,7 @@ import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox'; import { Checkbox } from './core/Checkbox';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
@@ -39,6 +40,7 @@ function ExportDataDialogContent({
allWorkspaces: Workspace[]; allWorkspaces: Workspace[];
activeWorkspace: Workspace; activeWorkspace: Workspace;
}) { }) {
const [includeEnvironments, setIncludeEnvironments] = useState<boolean>(true);
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({ const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
[activeWorkspace.id]: true, [activeWorkspace.id]: true,
}); });
@@ -67,10 +69,14 @@ function ExportDataDialogContent({
return; return;
} }
await invokeCmd('cmd_export_data', { workspaceIds: ids, exportPath }); await invokeCmd('cmd_export_data', {
workspaceIds: ids,
exportPath,
includeEnvironments: includeEnvironments,
});
onHide(); onHide();
onSuccess(exportPath); onSuccess(exportPath);
}, [onHide, onSuccess, selectedWorkspaces, workspaces]); }, [includeEnvironments, onHide, onSuccess, selectedWorkspaces, workspaces]);
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]); const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length; const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
@@ -117,6 +123,18 @@ function ExportDataDialogContent({
))} ))}
</tbody> </tbody>
</table> </table>
<Banner className="!p-0">
<details open>
<summary className="px-3 py-2">Extra Settings</summary>
<div className="px-3 pb-2">
<Checkbox
checked={includeEnvironments}
onChange={setIncludeEnvironments}
title="Include environments"
/>
</div>
</details>
</Banner>
<HStack space={2} justifyContent="end"> <HStack space={2} justifyContent="end">
<Button className="focus" variant="border" onClick={onHide}> <Button className="focus" variant="border" onClick={onHide}>
Cancel Cancel
@@ -128,7 +146,8 @@ function ExportDataDialogContent({
disabled={noneSelected} disabled={noneSelected}
onClick={() => handleExport()} onClick={() => handleExport()}
> >
Export {pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })} Export{' '}
{pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
</Button> </Button>
</HStack> </HStack>
</VStack> </VStack>

View File

@@ -89,7 +89,7 @@ export function SettingsGeneral() {
<Separator className="my-4" /> <Separator className="my-4" />
<Heading size={2}> <Heading level={2}>
Workspace{' '} Workspace{' '}
<div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink"> <div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink">
{workspace.name} {workspace.name}
@@ -134,7 +134,7 @@ export function SettingsGeneral() {
<Separator className="my-4" /> <Separator className="my-4" />
<Heading size={2}>App Info</Heading> <Heading level={2}>App Info</Heading>
<KeyValueRows> <KeyValueRows>
<KeyValueRow label="Version">{appInfo.version}</KeyValueRow> <KeyValueRow label="Version">{appInfo.version}</KeyValueRow>
<KeyValueRow <KeyValueRow

View File

@@ -84,7 +84,7 @@ export function Dialog({
)} )}
> >
{title ? ( {title ? (
<Heading className="px-6 mt-4 mb-2" size={1} id={titleId}> <Heading className="px-6 mt-4 mb-2" level={1} id={titleId}>
{title} {title}
</Heading> </Heading>
) : ( ) : (

View File

@@ -2,19 +2,19 @@ import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
interface Props extends HTMLAttributes<HTMLHeadingElement> { interface Props extends HTMLAttributes<HTMLHeadingElement> {
size?: 1 | 2 | 3; level?: 1 | 2 | 3;
} }
export function Heading({ className, size = 1, ...props }: Props) { export function Heading({ className, level = 1, ...props }: Props) {
const Component = size === 1 ? 'h1' : size === 2 ? 'h2' : 'h3'; const Component = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
return ( return (
<Component <Component
className={classNames( className={classNames(
className, className,
'font-semibold text', 'font-semibold text',
size === 1 && 'text-2xl', level === 1 && 'text-2xl',
size === 2 && 'text-xl', level === 2 && 'text-xl',
size === 3 && 'text-lg', level === 3 && 'text-lg',
)} )}
{...props} {...props}
/> />

View File

@@ -21,7 +21,7 @@ export function useExportData() {
showDialog({ showDialog({
id: 'export-data', id: 'export-data',
title: 'Export App Data', title: 'Export Data',
size: 'md', size: 'md',
noPadding: true, noPadding: true,
render: ({ hide }) => ( render: ({ hide }) => (