u
This commit is contained in:
@@ -7,7 +7,7 @@ description: >-
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 250 API endpoints across all modules.
|
Overview of all 249 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -922,9 +922,6 @@ _none_
|
|||||||
#### Restore system default applets — `POST /vfs/applets/restore`
|
#### Restore system default applets — `POST /vfs/applets/restore`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { success: bool, restored: str[], count: int }`
|
- Returns: `200 { success: bool, restored: str[], count: int }`
|
||||||
#### Respond to a frontend draft request — `POST /vfs/drafts/response`
|
|
||||||
- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }`
|
|
||||||
- Returns: `200 { success: bool }`
|
|
||||||
#### Create directory in virtual filesystem — `POST /vfs/mkdir`
|
#### Create directory in virtual filesystem — `POST /vfs/mkdir`
|
||||||
- Params: `body: { path: str }`
|
- Params: `body: { path: str }`
|
||||||
- Returns: `200 { success: bool, dir: str }`
|
- Returns: `200 { success: bool, dir: str }`
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import React, { PureComponent } from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
|
import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded';
|
||||||
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
|
import PictureAsPdfRoundedIcon from '@mui/icons-material/PictureAsPdfRounded';
|
||||||
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
|
import TableChartRoundedIcon from '@mui/icons-material/TableChartRounded';
|
||||||
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
||||||
@@ -202,6 +208,218 @@ export function parseAttachmentTag(attrString) {
|
|||||||
|
|
||||||
export { stripUserEnv };
|
export { stripUserEnv };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all `[EXTRACTION ...]` blocks (or legacy model instruction code blocks)
|
||||||
|
* and return structured extraction objects along with clean non-invoice text.
|
||||||
|
* @param {string} rawText
|
||||||
|
* @returns {{ extractions: Array<{ label: string, text: string }>, cleanText: string }}
|
||||||
|
*/
|
||||||
|
export function parseExtractionsFromText(rawText) {
|
||||||
|
if (typeof rawText !== 'string' || !rawText) {
|
||||||
|
return { extractions: [], cleanText: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractions = [];
|
||||||
|
let cleanText = rawText;
|
||||||
|
|
||||||
|
// 1. Explicit [EXTRACTION label="..."]...[/EXTRACTION] or <extraction label="...">...</extraction>
|
||||||
|
const tagRegex = /(?:\[EXTRACTION(?:\s+label="([^"]*)")?\]|<extraction(?:\s+label="([^"]*)")?>)([\s\S]*?)(?:\[\/EXTRACTION\]|<\/extraction>)/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = tagRegex.exec(rawText)) !== null) {
|
||||||
|
const label = match[1] || match[2] || 'parsed invoice';
|
||||||
|
let text = match[3] || '';
|
||||||
|
text = text.replace(/^```[a-z]*\s*\n?|\s*```$/gi, '').trim();
|
||||||
|
extractions.push({ label, text });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractions.length > 0) {
|
||||||
|
cleanText = cleanText.replace(tagRegex, '').trim();
|
||||||
|
return { extractions, cleanText };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Legacy / fallback: message contains "use 'Process Incoming Product and Invoices' SKILL" + code block
|
||||||
|
const instructionPhrase = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file";
|
||||||
|
if (cleanText.includes(instructionPhrase)) {
|
||||||
|
const codeBlockRegex = /```[a-z]*\s*\n([\s\S]*?)```/g;
|
||||||
|
let cbMatch;
|
||||||
|
while ((cbMatch = codeBlockRegex.exec(cleanText)) !== null) {
|
||||||
|
const code = (cbMatch[1] || '').trim();
|
||||||
|
if (code.length > 20) {
|
||||||
|
extractions.push({ label: 'parsed invoice', text: code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extractions.length > 0) {
|
||||||
|
cleanText = cleanText.replace(codeBlockRegex, '').trim();
|
||||||
|
return { extractions, cleanText };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Standalone code block (e.g. ```\n...invoice...```)
|
||||||
|
const soleCodeBlockMatch = cleanText.match(/^\s*```[a-z]*\s*\n([\s\S]{40,})```\s*$/);
|
||||||
|
if (soleCodeBlockMatch) {
|
||||||
|
const code = soleCodeBlockMatch[1].trim();
|
||||||
|
extractions.push({ label: 'parsed invoice', text: code });
|
||||||
|
cleanText = '';
|
||||||
|
return { extractions, cleanText };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { extractions: [], cleanText };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clickable pill representing an extracted invoice/data snippet inside a message bubble.
|
||||||
|
* Clicking opens a modal preview to inspect and copy the full text.
|
||||||
|
*/
|
||||||
|
export class ExtractionPill extends PureComponent {
|
||||||
|
state = {
|
||||||
|
openPreview: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = (e) => {
|
||||||
|
e?.preventDefault?.();
|
||||||
|
e?.stopPropagation?.();
|
||||||
|
this.setState({ openPreview: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
this.setState({ openPreview: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCopy = () => {
|
||||||
|
const { text } = this.props;
|
||||||
|
if (text && navigator?.clipboard?.writeText) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
enqueueSnackbar('Extrahierter Text kopiert', { variant: 'success' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label = 'parsed invoice', text = '', isUser = false } = this.props;
|
||||||
|
const { openPreview } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={this.handleClick}
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
my: 0.35,
|
||||||
|
mx: 0.2,
|
||||||
|
pl: 1.1,
|
||||||
|
pr: 1,
|
||||||
|
py: 0.45,
|
||||||
|
borderRadius: 2,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: isUser ? 'rgba(255, 255, 255, 0.4)' : 'divider',
|
||||||
|
bgcolor: isUser
|
||||||
|
? 'rgba(255, 255, 255, 0.2)'
|
||||||
|
: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)'),
|
||||||
|
color: isUser ? '#fff' : 'text.primary',
|
||||||
|
cursor: 'pointer',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
maxWidth: '100%',
|
||||||
|
outline: 'none',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: isUser ? 'rgba(255, 255, 255, 0.3)' : 'action.hover',
|
||||||
|
borderColor: isUser ? '#fff' : 'primary.main',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DescriptionRoundedIcon sx={{ fontSize: 16, color: isUser ? '#fff' : 'primary.main', flexShrink: 0 }} />
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'inherit',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
lineHeight: 1.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label || 'parsed invoice'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={openPreview}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
sx={(theme) => ({
|
||||||
|
zIndex: theme.zIndex.modal + 50,
|
||||||
|
})}
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
borderRadius: 2.5,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pb: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<DescriptionRoundedIcon sx={{ color: 'primary.main', fontSize: 22 }} />
|
||||||
|
<Typography variant="h6" component="span" sx={{ fontSize: '1.05rem', fontWeight: 600 }}>
|
||||||
|
{label || 'parsed invoice'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small" onClick={this.handleClose} aria-label="Schließen">
|
||||||
|
<CloseRoundedIcon sx={{ fontSize: 20 }} />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers sx={{ p: 2, bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.02)' }}>
|
||||||
|
<Box
|
||||||
|
component="pre"
|
||||||
|
sx={{
|
||||||
|
m: 0,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
overflow: 'auto',
|
||||||
|
maxHeight: '60vh',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 2.5, py: 1.5 }}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ContentCopyRoundedIcon sx={{ fontSize: 18 }} />}
|
||||||
|
onClick={this.handleCopy}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Kopieren
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.handleClose} color="inherit" size="small">
|
||||||
|
Schließen
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract all `[ATTACHMENT ...]` markers and return clean text plus attachment metadata.
|
* Extract all `[ATTACHMENT ...]` markers and return clean text plus attachment metadata.
|
||||||
* Also strips <USERENV> tags so route context is never rendered in chat views.
|
* Also strips <USERENV> tags so route context is never rendered in chat views.
|
||||||
|
|||||||
@@ -88,8 +88,36 @@ function formatUpdated(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function conversationTitle(c) {
|
function conversationTitle(c) {
|
||||||
const msg = (c.first_message || '').replace(/\s+/g, ' ').trim();
|
let msg = (c.first_message || '').trim();
|
||||||
return msg || '…';
|
if (!msg) return '…';
|
||||||
|
|
||||||
|
const extMatch = msg.match(/\[EXTRACTION(?:\s+label="([^"]*)")?\]|<extraction(?:\s+label="([^"]*)")?>/i);
|
||||||
|
const hasInvoiceSkill = msg.includes("Process Incoming Product and Invoices");
|
||||||
|
|
||||||
|
let stripped = msg
|
||||||
|
.replace(/\[EXTRACTION(?:\s+label="[^"]*")?\][\s\S]*?\[\/EXTRACTION\]/gi, '')
|
||||||
|
.replace(/<extraction(?:\s+label="[^"]*")?>[\s\S]*?<\/extraction>/gi, '')
|
||||||
|
.replace(/```[\s\S]*?```/g, '')
|
||||||
|
.replace(/use 'Process Incoming Product and Invoices' SKILL to fill drafts\/delivery vfs file/gi, '')
|
||||||
|
.replace(/<USERENV>[\s\S]*?<\/USERENV>/gi, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (extMatch || hasInvoiceSkill) {
|
||||||
|
const label = extMatch?.[1] || extMatch?.[2] || 'parsed invoice';
|
||||||
|
if (stripped) {
|
||||||
|
return `${label}: ${stripped}`;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clean = msg
|
||||||
|
.replace(/<USERENV>[\s\S]*?<\/USERENV>/gi, '')
|
||||||
|
.replace(/```[\s\S]*?```/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return clean || msg.slice(0, 40) || '…';
|
||||||
}
|
}
|
||||||
const MIN_DRAWER_WIDTH = 320;
|
const MIN_DRAWER_WIDTH = 320;
|
||||||
|
|
||||||
@@ -566,14 +594,17 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
const instructionText =
|
const instructionText =
|
||||||
uniqueInstructions.join('\n\n') || EXTRACTION_MODEL_INSTRUCTION;
|
uniqueInstructions.join('\n\n') || EXTRACTION_MODEL_INSTRUCTION;
|
||||||
|
|
||||||
const extMarkdown = extractions
|
const extBlocks = extractions
|
||||||
.map((ext) => `\`\`\`\n${ext.text.trim()}\n\`\`\``)
|
.map(
|
||||||
|
(ext) =>
|
||||||
|
`[EXTRACTION label="${ext.label || 'parsed invoice'}"]\n\`\`\`\n${ext.text.trim()}\n\`\`\`\n[/EXTRACTION]`
|
||||||
|
)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
text = `${instructionText}\n\n${text}\n\n${extMarkdown}`;
|
text = `${instructionText}\n\n${text}\n\n${extBlocks}`;
|
||||||
} else {
|
} else {
|
||||||
text = `${instructionText}\n\n${extMarkdown}`;
|
text = `${instructionText}\n\n${extBlocks}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1978,6 +2009,9 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
onClose={this.handleCloseExtractionPreview}
|
onClose={this.handleCloseExtractionPreview}
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
sx={(theme) => ({
|
||||||
|
zIndex: theme.zIndex.modal + 50,
|
||||||
|
})}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
paper: {
|
paper: {
|
||||||
sx: {
|
sx: {
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ import CalculateIcon from '@mui/icons-material/Calculate';
|
|||||||
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
|
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
|
||||||
import DirectionsIcon from '@mui/icons-material/Directions';
|
import DirectionsIcon from '@mui/icons-material/Directions';
|
||||||
import HealingMarkdown from './HealingMarkdown';
|
import HealingMarkdown from './HealingMarkdown';
|
||||||
import { VfsAttachmentLink, parseAttachmentsFromText, stripUserEnv } from './ChatAttachmentItems';
|
import {
|
||||||
|
VfsAttachmentLink,
|
||||||
|
ExtractionPill,
|
||||||
|
parseAttachmentsFromText,
|
||||||
|
parseExtractionsFromText,
|
||||||
|
stripUserEnv,
|
||||||
|
} from './ChatAttachmentItems';
|
||||||
import {
|
import {
|
||||||
CHROMA,
|
CHROMA,
|
||||||
SHADOW_BUBBLE_ASSISTANT,
|
SHADOW_BUBBLE_ASSISTANT,
|
||||||
@@ -878,7 +884,8 @@ function MessageRow({ msg, showTime = true }) {
|
|||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textContent);
|
const { extractions, cleanText: textAfterExtractions } = parseExtractionsFromText(textContent);
|
||||||
|
const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textAfterExtractions);
|
||||||
const cleanText = stripUserEnv(rawCleanText);
|
const cleanText = stripUserEnv(rawCleanText);
|
||||||
|
|
||||||
if (msg.role === 'system') {
|
if (msg.role === 'system') {
|
||||||
@@ -957,6 +964,26 @@ function MessageRow({ msg, showTime = true }) {
|
|||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{extractions.length > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 0.75,
|
||||||
|
mb: cleanText || vfsAttachments.length > 0 ? 0.75 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{extractions.map((ext, idx) => (
|
||||||
|
<ExtractionPill
|
||||||
|
key={ext.id || idx}
|
||||||
|
label={ext.label || 'parsed invoice'}
|
||||||
|
text={ext.text}
|
||||||
|
isUser={isUser}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{vfsAttachments.length > 0 && (
|
{vfsAttachments.length > 0 && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
lastRestoreResult: null, // null | { type: 'skills' | 'applets', count: number, restored: string[] }
|
lastRestoreResult: null, // null | { type: 'skills' | 'applets', count: number, restored: string[] }
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleRefresh = this.handleRefresh.bind(this);
|
|
||||||
this.handleTierChange = this.handleTierChange.bind(this);
|
this.handleTierChange = this.handleTierChange.bind(this);
|
||||||
this.handleToggleExpand = this.handleToggleExpand.bind(this);
|
this.handleToggleExpand = this.handleToggleExpand.bind(this);
|
||||||
this.handleOpenConfirm = this.handleOpenConfirm.bind(this);
|
this.handleOpenConfirm = this.handleOpenConfirm.bind(this);
|
||||||
@@ -145,18 +144,6 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region handleRefresh
|
|
||||||
// MARK: calls backupAutomat.actions.reload from ./api/index.js
|
|
||||||
async handleRefresh() {
|
|
||||||
try {
|
|
||||||
await backupAutomat.actions.reload();
|
|
||||||
enqueueSnackbar('Backup-Status aktualisiert', { variant: 'info' });
|
|
||||||
} catch (err) {
|
|
||||||
notifyError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
handleTierChange(_event, newValue) {
|
handleTierChange(_event, newValue) {
|
||||||
this.setState({ tierTab: newValue, expandedItemName: null });
|
this.setState({ tierTab: newValue, expandedItemName: null });
|
||||||
}
|
}
|
||||||
@@ -262,7 +249,7 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<RefreshIcon />}
|
startIcon={<RefreshIcon />}
|
||||||
onClick={this.handleRefresh}
|
onClick={() => void this.loadBackups()}
|
||||||
sx={{ alignSelf: 'flex-start' }}
|
sx={{ alignSelf: 'flex-start' }}
|
||||||
>
|
>
|
||||||
Erneut versuchen
|
Erneut versuchen
|
||||||
@@ -286,28 +273,15 @@ export default class BackupSettingsPanel extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
{/* Header Title & Actions */}
|
{/* Header Title */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2 }}>
|
<Box>
|
||||||
<Box>
|
<Typography variant="h6" fontWeight={700} sx={{ color: DARK_4, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Typography variant="h6" fontWeight={700} sx={{ color: DARK_4, display: 'flex', alignItems: 'center', gap: 1 }}>
|
<BackupOutlinedIcon sx={{ color: PRIMARY }} />
|
||||||
<BackupOutlinedIcon sx={{ color: PRIMARY }} />
|
Datensicherung & Backups
|
||||||
Datensicherung & Backups
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher.
|
||||||
Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher.
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
onClick={this.handleRefresh}
|
|
||||||
disabled={status === 'loading'}
|
|
||||||
>
|
|
||||||
Aktualisieren
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Status Badges Bar */}
|
{/* Status Badges Bar */}
|
||||||
|
|||||||
Reference in New Issue
Block a user