feat(export-import): add yaml export/import with id upsert for credentials, snippets, and scenarios
- all entities export a kind field (credential/snippet/scenario) for safe type checking on import - import upserts by id: overwrites if id exists, creates with explicit id otherwise - scenario export now includes id and step ids; import deletes old steps before recreating - add GET /:id/export and POST /import endpoints to credential and snippet controllers - add UuidBadge ui component: shortens uuid to first+last 4 hex chars, tooltip, clipboard copy with animated ClipboardCheck icon pop - apply UuidBadge across all entity id display sites (detail pages, card footers, table columns) - add export/import buttons to CredentialsPage, SnippetsPage, CredentialDetailPage, SnippetDetailPage
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { Download, Pencil, Trash2 } from 'lucide-react';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
import { credentials } from '../../api';
|
||||
import type { Credential } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp } from '../../ui';
|
||||
import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
|
||||
import styles from '../Page.module.css';
|
||||
|
||||
export function CredentialDetailPage() {
|
||||
@@ -18,7 +19,7 @@ export function CredentialDetailPage() {
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
credentials
|
||||
.get(Number(id))
|
||||
.get(id)
|
||||
.then(setCredential)
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -30,6 +31,18 @@ export function CredentialDetailPage() {
|
||||
navigate('/credentials');
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!credential) return;
|
||||
const data = await credentials.exportCredential(credential.id);
|
||||
const blob = new Blob([yamlStringify(data)], { type: 'application/yaml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `credential-${credential.name}.yaml`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.pageToolbar}>
|
||||
@@ -41,6 +54,10 @@ export function CredentialDetailPage() {
|
||||
/>
|
||||
{credential && (
|
||||
<div className={styles.toolbarActions}>
|
||||
<Button variant="secondary" size="sm" onClick={handleExport}>
|
||||
<Download size={14} />
|
||||
{t('credentials.action_export')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -74,7 +91,7 @@ export function CredentialDetailPage() {
|
||||
<DescriptionList
|
||||
layout="comfortable"
|
||||
items={[
|
||||
{ term: t('credentials.field_id'), detail: credential.id },
|
||||
{ term: t('credentials.field_id'), detail: <UuidBadge id={credential.id} /> },
|
||||
{
|
||||
term: t('credentials.field_last_used'),
|
||||
detail: credential.lastUsedAt ? (
|
||||
|
||||
Reference in New Issue
Block a user