Incident jde otevrit a posunout do dalsiho stavu
Seznam incidentu ukazoval jen titulek a casy, server pritom posilal impact i diagnostiku. Novy detail /dashboard/incidenty/🆔 co to znamena, ticket, zdroj, pro spravce platformy cela diagnostika s kopirovanim. Stav jde posunout (zkoumame, pricina znama, sledujeme, vyreseno), vyreseny vratit do sledovani. Nove pravo incident.manage, platformni incident meni jen spravce platformy, zmena jde do auditu. Endpointy GET /incidents/:id a PATCH /incidents/:id/status, OpenAPI a dokumentace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
bc6508e1f3
commit
42b3da8303
@@ -57,6 +57,8 @@ Vyzaduji `Authorization: Bearer <token>`:
|
||||
| GET | `/api/invites/:kod` |
|
||||
| POST | `/api/invites/:kod/accept` |
|
||||
| GET | `/api/dashboard/incidents` |
|
||||
| GET | `/api/dashboard/incidents/:id` |
|
||||
| PATCH | `/api/dashboard/incidents/:id/status` |
|
||||
| GET | `/api/dashboard/storage` |
|
||||
| GET | `/api/dashboard/services` |
|
||||
| GET | `/api/dashboard/connectors/services` |
|
||||
|
||||
@@ -226,6 +226,7 @@ a drzi se vsude, kde se neco zaklada:
|
||||
| pozvanka | `user.manage`, role jen z te firmy | `src/routes/invites.ts` |
|
||||
| konektor | `connector.manage` | `src/routes/connectors.ts`, `dashboard.ts` |
|
||||
| automatizace | `automation.edit` za firmu automatizace | `src/routes/dashboard.ts` |
|
||||
| stav incidentu | `incident.manage` za firmu incidentu, platformni jen spravce platformy | `src/routes/dashboard.ts` |
|
||||
| akce nad ticketem | pravo akce za firmu ticketu a strop viditelnosti | `src/routes/ticketActions.ts` |
|
||||
|
||||
Spravce firmy s `user.manage` ma **jen svou firmu**: nenastavi `platformAdmin`,
|
||||
|
||||
@@ -55,6 +55,7 @@ byla, ale `viewer` mohl zalozit konektor nebo smazat automatizaci. Ted:
|
||||
| pozvanky | `user.manage`, role jen z te firmy |
|
||||
| konektory (zalozeni, uprava, smazani, test) | `connector.manage` |
|
||||
| automatizace (zalozeni, uprava, smazani, novy token) | `automation.edit` |
|
||||
| stav incidentu | `incident.manage` za firmu incidentu; platformni jen spravce platformy |
|
||||
| vestavene akce na ticketu | pravo akce za firmu ticketu plus strop viditelnosti |
|
||||
| prepnuti na jiny ucet, audit | `impersonate`, `audit.view` |
|
||||
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-09 - Incident jde otevrit a posunout do dalsiho stavu
|
||||
|
||||
Seznam incidentu ukazoval jen titulek, sluzbu a casy. Server pritom uz posilal
|
||||
`impact` (co to pro klienta znamena) a spravci platformy `detail`
|
||||
(diagnostiku z behu), ale nic z toho se nekreslilo a nebylo kam kliknout.
|
||||
Stav incidentu slo menit jen v kodu.
|
||||
|
||||
- `GET /api/dashboard/incidents/:id` a stranka `/dashboard/incidenty/:id`:
|
||||
co to znamena, odkaz na ticket, zdroj (ID behu), a pro spravce platformy
|
||||
cela diagnostika s tlacitkem kopirovat. Cizi incident je 404.
|
||||
- `PATCH /api/dashboard/incidents/:id/status`: posun dopredu (zkoumame,
|
||||
pricina znama, sledujeme, vyreseno); vyreseny jde vratit do sledovani, kdyz
|
||||
se problem ozve znovu. Nove pravo `incident.manage` (spravce firmy ho ma
|
||||
automaticky pres `syncSystemRoles`); platformni incident bez firmy meni jen
|
||||
spravce platformy. Zmena jde do auditu jako `incident.status`.
|
||||
- Seznam ma odkaz na detail, ukazuje `impact` a ticket; sloupec "Post-mortem",
|
||||
ktery nic nedelal, je pryc.
|
||||
|
||||
## 2026-09-09 - Prepinani jazyku docasne schovane
|
||||
|
||||
Do rozhodnuti o nove znacce je web jen cesky. `MULTILANG_ENABLED = false`
|
||||
|
||||
@@ -124,6 +124,12 @@ export function listIncidents(tenantIds: string[]): Incident[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** Incident podle ID. Cizi firmy se chova jako neexistujici, stejne jako u ticketu. */
|
||||
export function findIncident(id: string, tenantIds: string[]): Incident | undefined {
|
||||
const incident = incidents.find((item) => item.id === id);
|
||||
return incident && visibleTo(incident, tenantIds) ? incident : undefined;
|
||||
}
|
||||
|
||||
/** Uz je stejny problem otevreny? Aby deset stejnych chyb nedelalo deset incidentu. */
|
||||
export function findOpenIncident(source: string, tenantIds: string[]): Incident | undefined {
|
||||
return incidents.find(
|
||||
|
||||
@@ -68,6 +68,12 @@ export const permissionCatalog: PermissionDefinition[] = [
|
||||
},
|
||||
{ key: 'action.manage', label: 'Spravovat definice akcí', group: 'Nastavení firmy' },
|
||||
{ key: 'automation.edit', label: 'Upravovat automatizace', group: 'Nastavení firmy' },
|
||||
{
|
||||
key: 'incident.manage',
|
||||
label: 'Měnit stav incidentu',
|
||||
group: 'Nastavení firmy',
|
||||
hint: 'Posouvat incident firmy do dalšího stavu až po vyřešení. Platformní incidenty mění jen správce platformy.',
|
||||
},
|
||||
{ key: 'connector.manage', label: 'Spravovat konektory', group: 'Nastavení firmy' },
|
||||
{ key: 'widget.manage', label: 'Spravovat vlastní widgety', group: 'Nastavení firmy' },
|
||||
{ key: 'group.manage', label: 'Spravovat skupiny řešitelů', group: 'Nastavení firmy' },
|
||||
|
||||
@@ -2274,6 +2274,48 @@ export function buildOpenApiDocument() {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/incidents/{id}': {
|
||||
get: {
|
||||
tags: ['Dashboard'],
|
||||
summary: 'Detail incidentu',
|
||||
description:
|
||||
'Incident firmy nebo platformni. `detail` (diagnostika) dostane jen spravce platformy, ' +
|
||||
'ostatni maji null. Cizi incident je 404.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [idParam, tenantParam],
|
||||
responses: {
|
||||
'200': jsonResponse('Incident', { $ref: '#/components/schemas/Incident' }),
|
||||
'404': { description: 'Incident neexistuje nebo patri jine firme' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/incidents/{id}/status': {
|
||||
patch: {
|
||||
tags: ['Dashboard'],
|
||||
summary: 'Posunout incident do dalsiho stavu',
|
||||
description:
|
||||
'Pravo incident.manage za firmu incidentu; platformni incident (bez firmy) meni jen ' +
|
||||
'spravce platformy. Stav resolved nastavi resolvedAt.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [idParam, tenantParam],
|
||||
requestBody: jsonBody({
|
||||
type: 'object',
|
||||
required: ['status'],
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['investigating', 'identified', 'monitoring', 'resolved'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
responses: {
|
||||
'200': jsonResponse('Incident', { $ref: '#/components/schemas/Incident' }),
|
||||
'400': { description: 'Neznamy stav' },
|
||||
'403': { description: 'Chybi pravo incident.manage' },
|
||||
'404': { description: 'Incident neexistuje nebo patri jine firme' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/stream': {
|
||||
get: {
|
||||
tags: ['Dashboard'],
|
||||
|
||||
+63
-1
@@ -39,7 +39,13 @@ import {
|
||||
} from '../data/dashboardLayouts.js';
|
||||
import { collectScopes } from '../data/flowScope.js';
|
||||
import { widgetCatalog } from '../data/widgets.js';
|
||||
import { createIncident, listIncidents } from '../data/incidentStore.js';
|
||||
import {
|
||||
createIncident,
|
||||
findIncident,
|
||||
listIncidents,
|
||||
updateIncidentStatus,
|
||||
type Incident,
|
||||
} from '../data/incidentStore.js';
|
||||
import { getSummary } from '../data/mock.js';
|
||||
import { isMember, listGroups, listPeople } from '../data/people.js';
|
||||
import { hasPermission } from '../data/permissions.js';
|
||||
@@ -145,6 +151,62 @@ dashboardRouter.get('/incidents', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/** Stejne pravidlo jako u seznamu: diagnostiku vidi jen spravce platformy. */
|
||||
function publicIncident(req: Request, incident: Incident): Incident {
|
||||
return { ...incident, detail: req.user!.platformAdmin ? incident.detail : null };
|
||||
}
|
||||
|
||||
dashboardRouter.get('/incidents/:id', (req, res) => {
|
||||
const scope = scopeOrDeny(req, res);
|
||||
if (!scope) return;
|
||||
|
||||
const incident = findIncident(req.params.id, scope.tenantIds);
|
||||
if (!incident) return res.status(404).json({ error: 'not_found', message: 'Incident neexistuje.' });
|
||||
return res.json(publicIncident(req, incident));
|
||||
});
|
||||
|
||||
const incidentStatusSchema = z.object({
|
||||
status: z.enum(['investigating', 'identified', 'monitoring', 'resolved']),
|
||||
});
|
||||
|
||||
/**
|
||||
* Posun incidentu do dalsiho stavu.
|
||||
*
|
||||
* Incident firmy meni, kdo ma v te firme `incident.manage`. Platformni incident
|
||||
* (bez firmy) je nas a meni ho jen spravce platformy - klient by jinak mohl
|
||||
* "vyresit" vypadek, ktery se tyka vsech.
|
||||
*/
|
||||
dashboardRouter.patch('/incidents/:id/status', (req, res) => {
|
||||
const scope = scopeOrDeny(req, res);
|
||||
if (!scope) return;
|
||||
|
||||
const incident = findIncident(req.params.id, scope.tenantIds);
|
||||
if (!incident) return res.status(404).json({ error: 'not_found', message: 'Incident neexistuje.' });
|
||||
|
||||
const allowed =
|
||||
req.user!.platformAdmin ||
|
||||
(incident.tenantId !== null && hasPermission(req.user!, 'incident.manage', incident.tenantId));
|
||||
if (!allowed) {
|
||||
return res.status(403).json({ error: 'forbidden', message: 'Stav incidentu nemůžete měnit.' });
|
||||
}
|
||||
|
||||
const parsed = incidentStatusSchema.safeParse(req.body);
|
||||
if (!parsed.success) return validationError(res, parsed.error);
|
||||
|
||||
const updated = updateIncidentStatus(incident.id, parsed.data.status);
|
||||
if (!updated) return res.status(404).json({ error: 'not_found', message: 'Incident neexistuje.' });
|
||||
|
||||
recordAudit({
|
||||
userId: req.user!.id,
|
||||
userEmail: req.user!.email,
|
||||
tenantId: incident.tenantId,
|
||||
action: 'incident.status',
|
||||
target: incident.id,
|
||||
detail: { status: parsed.data.status },
|
||||
});
|
||||
return res.json(publicIncident(req, updated));
|
||||
});
|
||||
|
||||
/**
|
||||
* Hlaseni padu portalu.
|
||||
*
|
||||
|
||||
@@ -33,6 +33,7 @@ const Tickets = lazy(() => import('@/pages/dashboard/Tickets'));
|
||||
const Helpdesk = lazy(() => import('@/pages/dashboard/Helpdesk'));
|
||||
const TicketDetail = lazy(() => import('@/pages/dashboard/TicketDetail'));
|
||||
const Incidents = lazy(() => import('@/pages/dashboard/Incidents'));
|
||||
const IncidentDetail = lazy(() => import('@/pages/dashboard/IncidentDetail'));
|
||||
const Settings = lazy(() => import('@/pages/dashboard/Settings'));
|
||||
const Actions = lazy(() => import('@/pages/dashboard/Actions'));
|
||||
const ActionDetail = lazy(() => import('@/pages/dashboard/ActionDetail'));
|
||||
@@ -88,6 +89,7 @@ export default function App() {
|
||||
<Route path="helpdesk" element={<Helpdesk />} />
|
||||
<Route path="tickety/:id" element={<TicketDetail />} />
|
||||
<Route path="incidenty" element={<Incidents />} />
|
||||
<Route path="incidenty/:id" element={<IncidentDetail />} />
|
||||
<Route path="nastaveni" element={<Settings />} />
|
||||
<Route path="akce" element={<Actions />} />
|
||||
<Route path="akce/:id" element={<ActionDetail />} />
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ArrowLeft, Check, Copy } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
import { IncidentSeverityBadge, IncidentStatusBadge } from '@/components/dashboard/StatusBadge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useAccess } from '@/lib/collections';
|
||||
import { formatDateTime, formatRelative } from '@/lib/format';
|
||||
import { useApiQuery } from '@/lib/useApiQuery';
|
||||
import { usePageMeta } from '@/lib/usePageMeta';
|
||||
import { useSubmit } from '@/lib/useSubmit';
|
||||
import type { Incident, IncidentStatus } from '@/types/dashboard';
|
||||
|
||||
/**
|
||||
* Detail incidentu.
|
||||
*
|
||||
* Dve vrstvy, jak je ma i zaznam: `impact` je pro klienta (co to pro nej
|
||||
* znamena), `detail` je diagnostika pro spravce platformy - server ho
|
||||
* ostatnim posila jako null, takze se tady jen nekresli.
|
||||
*
|
||||
* Stav se posouva dopredu po krocich (zkoumame, pricina znama, sledujeme,
|
||||
* vyreseno). Vyreseny incident jde vratit do "sledujeme", kdyz se problem
|
||||
* ozve znovu - bez toho by musel vzniknout novy a historie by se roztrhla.
|
||||
*/
|
||||
|
||||
const STATUS_FLOW: IncidentStatus[] = ['investigating', 'identified', 'monitoring', 'resolved'];
|
||||
|
||||
const STATUS_LABEL: Record<IncidentStatus, string> = {
|
||||
investigating: 'Zkoumáme',
|
||||
identified: 'Příčina známa',
|
||||
monitoring: 'Sledujeme',
|
||||
resolved: 'Vyřešeno',
|
||||
};
|
||||
|
||||
/** Kam se da z daneho stavu posunout. */
|
||||
function nextStatuses(current: IncidentStatus): IncidentStatus[] {
|
||||
if (current === 'resolved') return ['monitoring'];
|
||||
const index = STATUS_FLOW.indexOf(current);
|
||||
return STATUS_FLOW.slice(index + 1);
|
||||
}
|
||||
|
||||
export default function IncidentDetail() {
|
||||
const { id = '' } = useParams();
|
||||
const { user } = useAuth();
|
||||
const access = useAccess();
|
||||
const incident = useApiQuery<Incident>(`/api/dashboard/incidents/${id}`, {
|
||||
refetchOn: ['incident.updated', 'incident.resolved'],
|
||||
});
|
||||
|
||||
usePageMeta({ title: incident.data ? `Incident ${incident.data.id}` : 'Incident', area: 'portal' });
|
||||
|
||||
const canManage =
|
||||
user?.platformAdmin === true ||
|
||||
(incident.data?.tenantId !== null && (access.data?.permissions ?? []).includes('incident.manage'));
|
||||
|
||||
const change = useSubmit(async (status: IncidentStatus) => {
|
||||
await apiFetch(`/api/dashboard/incidents/${id}/status`, { method: 'PATCH', body: { status } });
|
||||
incident.reload();
|
||||
}, 'Stav se nepodařilo změnit.');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Link
|
||||
to="/dashboard/incidenty"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
Incidenty
|
||||
</Link>
|
||||
|
||||
<DataState
|
||||
loading={incident.loading}
|
||||
refreshing={incident.refreshing}
|
||||
error={incident.error}
|
||||
onRetry={incident.reload}
|
||||
>
|
||||
{incident.data && (
|
||||
<div className="space-y-6">
|
||||
<header className="glass rounded-card p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<span className="font-mono text-xs text-white/40">{incident.data.id}</span>
|
||||
<IncidentSeverityBadge severity={incident.data.severity} />
|
||||
{incident.data.tenantId === null && (
|
||||
<span className="text-xs text-white/40">platformní, týká se všech firem</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="mt-2 text-2xl font-bold text-white">{incident.data.title}</h1>
|
||||
<p className="mt-1 text-sm text-white/50">Služba: {incident.data.service}</p>
|
||||
</div>
|
||||
<IncidentStatusBadge status={incident.data.status} />
|
||||
</div>
|
||||
|
||||
<dl className="mt-5 grid gap-4 border-t border-ink-600/50 pt-4 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<dt className="text-xs text-white/35">Začátek</dt>
|
||||
<dd className="mt-0.5 text-white/70">{formatDateTime(incident.data.startedAt)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-white/35">Trvání</dt>
|
||||
<dd className="mt-0.5 text-white/70">
|
||||
{incident.data.resolvedAt
|
||||
? `vyřešeno ${formatRelative(incident.data.resolvedAt)}`
|
||||
: `běží ${formatRelative(incident.data.startedAt).replace('před ', '')}`}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-white/35">Ticket</dt>
|
||||
<dd className="mt-0.5 text-white/70">
|
||||
{incident.data.ticketId ? (
|
||||
<Link
|
||||
to={`/dashboard/tickety/${incident.data.ticketId}`}
|
||||
className="font-mono hover:underline"
|
||||
>
|
||||
{incident.data.ticketId}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-white/35">bez ticketu</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<section className="glass rounded-card p-6">
|
||||
<h2 className="text-sm font-semibold text-white">Co to znamená</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-white/75">
|
||||
{incident.data.impact || 'Bez dalšího popisu.'}
|
||||
</p>
|
||||
{incident.data.source && (
|
||||
<p className="mt-3 text-xs text-white/40">
|
||||
Zdroj: <span className="font-mono">{incident.data.source}</span>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{incident.data.detail && <Diagnostics text={incident.data.detail} />}
|
||||
|
||||
{canManage && (
|
||||
<section className="glass rounded-card p-6">
|
||||
<h2 className="text-sm font-semibold text-white">Stav</h2>
|
||||
<p className="mt-1 text-xs text-white/45">
|
||||
Posunout do dalšího stavu. Vyřešený incident jde vrátit do sledování, když se
|
||||
problém ozve znovu.
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{nextStatuses(incident.data.status).map((status) => (
|
||||
<Button
|
||||
key={status}
|
||||
size="sm"
|
||||
variant={status === 'resolved' ? 'primary' : 'secondary'}
|
||||
disabled={change.busy}
|
||||
onClick={() => void change.submit(status)}
|
||||
>
|
||||
{STATUS_LABEL[status]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{change.error && <p className="mt-2 text-sm text-red-300">{change.error}</p>}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DataState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostika pro spravce platformy: cele hlaseni vcetne ID behu a dat na
|
||||
* vstupu, nic se nezkracuje. Tlacitko kopirovani, protoze prave tohle se
|
||||
* posila dal tomu, kdo to bude opravovat.
|
||||
*/
|
||||
function Diagnostics({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch (err) {
|
||||
console.warn('[incident] kopirovani selhalo:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="glass rounded-card p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold text-white">Diagnostika (jen správce platformy)</h2>
|
||||
<Button size="sm" variant="ghost" onClick={() => void copy()}>
|
||||
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
||||
{copied ? 'Zkopírováno' : 'Kopírovat'}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="mt-3 max-h-[32rem] overflow-auto rounded-lg bg-ink-950 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-white/80">
|
||||
{text}
|
||||
</pre>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
import { IncidentSeverityBadge, IncidentStatusBadge } from '@/components/dashboard/StatusBadge';
|
||||
import { formatDateTime, formatRelative } from '@/lib/format';
|
||||
@@ -17,7 +19,8 @@ export default function Incidents() {
|
||||
<header>
|
||||
<h1 className="text-2xl font-bold text-white">Incidenty</h1>
|
||||
<p className="mt-1 text-sm text-white/50">
|
||||
Výpadky a degradace služeb. Timeline zásahů a post-mortem doplníme v další iteraci.
|
||||
Výpadky a chyby automatizací. Detail říká, co to pro vás znamená, a správce ho posune
|
||||
do dalšího stavu.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -37,10 +40,27 @@ export default function Incidents() {
|
||||
<span className="font-mono text-xs text-white/40">{incident.id}</span>
|
||||
<IncidentSeverityBadge severity={incident.severity} />
|
||||
</div>
|
||||
<h2 className="mt-2 font-semibold text-white">{incident.title}</h2>
|
||||
<h2 className="mt-2 font-semibold text-white">
|
||||
<Link
|
||||
to={`/dashboard/incidenty/${incident.id}`}
|
||||
className="hover:text-brand-300 hover:underline"
|
||||
>
|
||||
{incident.title}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-white/50">Služba: {incident.service}</p>
|
||||
{incident.impact && <p className="mt-2 text-sm text-white/70">{incident.impact}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<IncidentStatusBadge status={incident.status} />
|
||||
<Link
|
||||
to={`/dashboard/incidenty/${incident.id}`}
|
||||
className="inline-flex items-center gap-1 text-xs text-white/50 hover:text-white"
|
||||
>
|
||||
Detail
|
||||
<ChevronRight className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<IncidentStatusBadge status={incident.status} />
|
||||
</div>
|
||||
|
||||
<dl className="mt-5 grid gap-4 border-t border-ink-600/50 pt-4 text-sm sm:grid-cols-3">
|
||||
@@ -57,9 +77,15 @@ export default function Incidents() {
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-white/35">Post-mortem</dt>
|
||||
<dt className="text-xs text-white/35">Ticket</dt>
|
||||
<dd className="mt-0.5 text-white/70">
|
||||
{incident.status === 'resolved' ? 'k dispozici' : 'po vyřešení'}
|
||||
{incident.ticketId ? (
|
||||
<Link to={`/dashboard/tickety/${incident.ticketId}`} className="font-mono hover:underline">
|
||||
{incident.ticketId}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-white/35">bez ticketu</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
Reference in New Issue
Block a user