mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
feat(app): migrate app parent view to react [EE-5361] (#10086)
Co-authored-by: testa113 <testa113>
This commit is contained in:
parent
531f88b947
commit
841ca1ebd4
42 changed files with 1448 additions and 810 deletions
|
@ -1,4 +1,4 @@
|
|||
import { User, Clock, Edit, ChevronRight, ChevronUp } from 'lucide-react';
|
||||
import { User, Clock, Info } from 'lucide-react';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pod } from 'kubernetes-types/core/v1';
|
||||
|
@ -10,7 +10,11 @@ import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
|||
import { DetailsTable } from '@@/DetailsTable';
|
||||
import { Badge } from '@@/Badge';
|
||||
import { Link } from '@@/Link';
|
||||
import { Button, LoadingButton } from '@@/buttons';
|
||||
import { LoadingButton } from '@@/buttons';
|
||||
import { WidgetBody, Widget } from '@@/Widget';
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Note } from '@@/Note';
|
||||
|
||||
import { isSystemNamespace } from '../../namespaces/utils';
|
||||
import {
|
||||
|
@ -44,13 +48,12 @@ export function ApplicationSummaryWidget() {
|
|||
endpointId: environmentId,
|
||||
},
|
||||
} = stateAndParams;
|
||||
const applicationQuery = useApplication(
|
||||
const { data: application, ...applicationQuery } = useApplication(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
resourceType
|
||||
);
|
||||
const application = applicationQuery.data;
|
||||
const systemNamespace = isSystemNamespace(namespace);
|
||||
const externalApplication = application && isExternalApplication(application);
|
||||
const applicationRequests = application && getResourceRequests(application);
|
||||
|
@ -59,7 +62,6 @@ export function ApplicationSummaryWidget() {
|
|||
const applicationNote =
|
||||
application?.metadata?.annotations?.[appNoteAnnotation];
|
||||
|
||||
const [isNoteOpen, setIsNoteOpen] = useState(true);
|
||||
const [applicationNoteFormValues, setApplicationNoteFormValues] =
|
||||
useState('');
|
||||
|
||||
|
@ -67,6 +69,9 @@ export function ApplicationSummaryWidget() {
|
|||
setApplicationNoteFormValues(applicationNote || '');
|
||||
}, [applicationNote]);
|
||||
|
||||
const failedCreateCondition = application?.status?.conditions?.find(
|
||||
(condition) => condition.reason === 'FailedCreate'
|
||||
);
|
||||
const patchApplicationMutation = usePatchApplicationMutation(
|
||||
environmentId,
|
||||
namespace,
|
||||
|
@ -74,191 +79,197 @@ export function ApplicationSummaryWidget() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<DetailsTable>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<div
|
||||
className="flex items-center gap-x-2"
|
||||
data-cy="k8sAppDetail-appName"
|
||||
>
|
||||
{name}
|
||||
{externalApplication && !systemNamespace && (
|
||||
<Badge type="info">external</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack</td>
|
||||
<td data-cy="k8sAppDetail-stackName">
|
||||
{application?.metadata?.labels?.[appStackNameLabel] || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namespace</td>
|
||||
<td>
|
||||
<div
|
||||
className="flex items-center gap-x-2"
|
||||
data-cy="k8sAppDetail-resourcePoolName"
|
||||
>
|
||||
<Link
|
||||
to="kubernetes.resourcePools.resourcePool"
|
||||
params={{ id: namespace }}
|
||||
>
|
||||
{namespace}
|
||||
</Link>
|
||||
{systemNamespace && <Badge type="info">system</Badge>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Application type</td>
|
||||
<td data-cy="k8sAppDetail-appType">{application?.kind || '-'}</td>
|
||||
</tr>
|
||||
{application?.kind && (
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
{applicationIsKind<Pod>('Pod', application) && (
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{application?.status?.phase}
|
||||
</td>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
{applicationQuery.isLoading && (
|
||||
<InlineLoader>Loading application...</InlineLoader>
|
||||
)}
|
||||
{!applicationIsKind<Pod>('Pod', application) && (
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{appKindToDeploymentTypeMap[application.kind]}
|
||||
<code className="ml-1">
|
||||
{getRunningPods(application)}
|
||||
</code> / <code>{getTotalPods(application)}</code>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)}
|
||||
{(!!applicationRequests?.cpu || !!applicationRequests?.memoryBytes) && (
|
||||
<tr>
|
||||
<td>
|
||||
Resource reservations
|
||||
{!applicationIsKind<Pod>('Pod', application) && (
|
||||
<div className="text-muted small">per instance</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!!applicationRequests?.cpu && (
|
||||
<div data-cy="k8sAppDetail-cpuReservation">
|
||||
CPU {applicationRequests.cpu}
|
||||
</div>
|
||||
)}
|
||||
{!!applicationRequests?.memoryBytes && (
|
||||
<div data-cy="k8sAppDetail-memoryReservation">
|
||||
Memory{' '}
|
||||
{bytesToReadableFormat(applicationRequests.memoryBytes)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{applicationOwner && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-owner"
|
||||
>
|
||||
<User />
|
||||
{applicationOwner}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-creationDate"
|
||||
>
|
||||
<Clock />
|
||||
{moment(application?.metadata?.creationTimestamp).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}
|
||||
</span>
|
||||
{(!externalApplication || systemNamespace) && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-creationMethod"
|
||||
>
|
||||
<Clock />
|
||||
Deployed from {applicationDeployMethod}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<form className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12 vertical-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="button"
|
||||
color="none"
|
||||
data-cy="k8sAppDetail-expandNoteButton"
|
||||
onClick={() => setIsNoteOpen(!isNoteOpen)}
|
||||
className="!m-0 !p-0"
|
||||
{application && (
|
||||
<>
|
||||
{failedCreateCondition && (
|
||||
<div
|
||||
className="vertical-center alert alert-danger mb-2"
|
||||
data-cy="k8sAppDetail-failedCreateMessage"
|
||||
>
|
||||
{isNoteOpen ? <ChevronUp /> : <ChevronRight />} <Edit />{' '}
|
||||
Note
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isNoteOpen && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<textarea
|
||||
className="form-control resize-y"
|
||||
name="application_note"
|
||||
id="application_note"
|
||||
value={applicationNoteFormValues}
|
||||
onChange={(e) =>
|
||||
setApplicationNoteFormValues(e.target.value)
|
||||
}
|
||||
rows={5}
|
||||
placeholder="Enter a note about this application..."
|
||||
/>
|
||||
<Icon icon={Info} className="mr-1" mode="danger" />
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
Failed to create application
|
||||
</div>
|
||||
{failedCreateCondition.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Authorized authorizations="K8sApplicationDetailsW">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
color="primary"
|
||||
size="small"
|
||||
className="!ml-0"
|
||||
type="button"
|
||||
onClick={() => patchApplicationNote()}
|
||||
disabled={
|
||||
// disable if there is no change to the note, or it's updating
|
||||
applicationNoteFormValues ===
|
||||
(applicationNote || '') ||
|
||||
patchApplicationMutation.isLoading
|
||||
}
|
||||
data-cy="k8sAppDetail-saveNoteButton"
|
||||
isLoading={patchApplicationMutation.isLoading}
|
||||
loadingText={applicationNote ? 'Updating' : 'Saving'}
|
||||
>
|
||||
{applicationNote ? 'Update' : 'Save'} note
|
||||
</LoadingButton>
|
||||
)}
|
||||
<DetailsTable>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<div
|
||||
className="flex items-center gap-x-2"
|
||||
data-cy="k8sAppDetail-appName"
|
||||
>
|
||||
{name}
|
||||
{externalApplication && !systemNamespace && (
|
||||
<Badge type="info">external</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</DetailsTable>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack</td>
|
||||
<td data-cy="k8sAppDetail-stackName">
|
||||
{application?.metadata?.labels?.[appStackNameLabel] ||
|
||||
'-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namespace</td>
|
||||
<td>
|
||||
<div
|
||||
className="flex items-center gap-x-2"
|
||||
data-cy="k8sAppDetail-resourcePoolName"
|
||||
>
|
||||
<Link
|
||||
to="kubernetes.resourcePools.resourcePool"
|
||||
params={{ id: namespace }}
|
||||
>
|
||||
{namespace}
|
||||
</Link>
|
||||
{systemNamespace && <Badge type="info">system</Badge>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Application type</td>
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{application?.kind || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
{application?.kind && (
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
{applicationIsKind<Pod>('Pod', application) && (
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{application?.status?.phase}
|
||||
</td>
|
||||
)}
|
||||
{!applicationIsKind<Pod>('Pod', application) && (
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{appKindToDeploymentTypeMap[application.kind]}
|
||||
<code className="ml-1">
|
||||
{getRunningPods(application)}
|
||||
</code>{' '}
|
||||
/ <code>{getTotalPods(application)}</code>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)}
|
||||
{(!!applicationRequests?.cpu ||
|
||||
!!applicationRequests?.memoryBytes) && (
|
||||
<tr>
|
||||
<td>
|
||||
Resource reservations
|
||||
{!applicationIsKind<Pod>('Pod', application) && (
|
||||
<div className="text-muted small">per instance</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!!applicationRequests?.cpu && (
|
||||
<div data-cy="k8sAppDetail-cpuReservation">
|
||||
CPU {applicationRequests.cpu}
|
||||
</div>
|
||||
)}
|
||||
{!!applicationRequests?.memoryBytes && (
|
||||
<div data-cy="k8sAppDetail-memoryReservation">
|
||||
Memory{' '}
|
||||
{bytesToReadableFormat(
|
||||
applicationRequests.memoryBytes
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{applicationOwner && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-owner"
|
||||
>
|
||||
<User />
|
||||
{applicationOwner}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-creationDate"
|
||||
>
|
||||
<Clock />
|
||||
{moment(
|
||||
application?.metadata?.creationTimestamp
|
||||
).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
{(!externalApplication || systemNamespace) && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
data-cy="k8sAppDetail-creationMethod"
|
||||
>
|
||||
<Clock />
|
||||
Deployed from {applicationDeployMethod}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<form className="form-horizontal">
|
||||
<Note
|
||||
value={applicationNoteFormValues}
|
||||
onChange={setApplicationNoteFormValues}
|
||||
defaultIsOpen
|
||||
isExpandable
|
||||
/>
|
||||
<Authorized authorizations="K8sApplicationDetailsW">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
color="primary"
|
||||
size="small"
|
||||
className="!ml-0"
|
||||
type="button"
|
||||
onClick={() => patchApplicationNote()}
|
||||
disabled={
|
||||
// disable if there is no change to the note, or it's updating
|
||||
applicationNoteFormValues ===
|
||||
(applicationNote || '') ||
|
||||
patchApplicationMutation.isLoading
|
||||
}
|
||||
data-cy="k8sAppDetail-saveNoteButton"
|
||||
isLoading={patchApplicationMutation.isLoading}
|
||||
loadingText={
|
||||
applicationNote ? 'Updating' : 'Saving'
|
||||
}
|
||||
>
|
||||
{applicationNote ? 'Update' : 'Save'} note
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</DetailsTable>
|
||||
</>
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue