refactor(kube): events datatable react migration [EE-6450] (#11583)

Co-authored-by: testa113 <testa113>
pull/11638/head
Ali 7 months ago committed by GitHub
parent c15789eb73
commit bb61e73464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,150 +0,0 @@
<div class="datatable">
<div class="toolBar">
<div class="toolBarTitle flex">
<div class="widget-icon space-right">
<pr-icon icon="'history'"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar">
<pr-icon icon="'search'" class="vertical-center"></pr-icon>
<input
type="text"
data-cy="k8sConfigDetail-eventsTableSearchInput"
class="searchInput ml-1"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for an event..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><pr-icon icon="'more-vertical'"></pr-icon></span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader"> Table settings </div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sConfigDetail-eventsTableAutoRefreshCheckbox"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate"> Refresh rate </label>
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sConfigDetail-eventsTableRefreshRateSelect"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<pr-icon id="refreshRateChange" style="display: none" icon="'check'" mode="'success'"></pr-icon>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table" data-cy="k8sConfigDetail-eventsTable">
<thead>
<tr>
<th>
<table-column-header
col-title="'Date'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Date'"
is-sorted-desc="$ctrl.state.orderBy === 'Date' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Date')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Kind'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Involved.kind'"
is-sorted-desc="$ctrl.state.orderBy === 'Involved.kind' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Involved.kind')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Type'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Type'"
is-sorted-desc="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Type')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Message'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Message'"
is-sorted-desc="$ctrl.state.orderBy === 'Message' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Message')"
></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
pagination-id="$ctrl.tableKey"
>
<td>{{ item.Date | getisodate }}</td>
<td>{{ item.Involved.kind }}</td>
<td
><span class="label label-{{ item.Type | kubernetesEventTypeColor }}">{{ item.Type }}</span></td
>
<td>{{ item.Message }}</td>
</tr>
<tr ng-if="$ctrl.loading">
<td colspan="4" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="!$ctrl.loading && (!$ctrl.dataset || $ctrl.state.filteredDataSet.length === 0)">
<td colspan="4" class="text-muted text-center">No event available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="k8sConfigDetail-eventsTablePaginationDropdown">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</div>

@ -1,13 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesEventsDatatable', {
templateUrl: './eventsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
loading: '<',
refreshCallback: '<',
},
});

@ -4,6 +4,8 @@ import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { NodeApplicationsDatatable } from '@/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable';
import { ResourceEventsDatatable } from '@/react/kubernetes/components/EventsDatatable/ResourceEventsDatatable';
import { withReactQuery } from '@/react-tools/withReactQuery';
export const clusterManagementModule = angular
.module('portainer.kubernetes.react.components.clusterManagement', [])
@ -14,4 +16,11 @@ export const clusterManagementModule = angular
'isLoading',
'onRefresh',
])
)
.component(
'resourceEventsDatatable',
r2a(
withUIRouter(withReactQuery(withCurrentUser(ResourceEventsDatatable))),
['resourceId', 'storageKey', 'namespace']
)
).name;

@ -251,16 +251,7 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
dataset="ctrl.events"
table-key="kubernetes.node.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
>
</kubernetes-events-datatable>
<resource-events-datatable resource-id="ctrl.node.Id" storage-key="'kubernetes.node.events'"></resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.node.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading class="vertical-center">

@ -320,7 +320,7 @@ class KubernetesNodeController {
try {
this.state.eventsLoading = true;
const events = await this.KubernetesEventService.get();
this.events = events.filter((item) => item.Involved.kind === 'Node');
this.events = events.filter((item) => item.Involved.uid === this.node.Id);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve node events');

@ -55,17 +55,11 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
title-icon="history"
dataset="ctrl.events"
table-key="kubernetes.configuration.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
>
</kubernetes-events-datatable>
<resource-events-datatable
resource-id="ctrl.configuration.Id"
storage-key="'kubernetes.configmap.events'"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
<uib-tab-heading>

@ -62,17 +62,11 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
title-icon="history"
dataset="ctrl.events"
table-key="kubernetes.configuration.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
>
</kubernetes-events-datatable>
<resource-events-datatable
resource-id="ctrl.configuration.Id"
storage-key="'kubernetes.secret.events'"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
<uib-tab-heading>

@ -286,15 +286,7 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
dataset="ctrl.events"
table-key="kubernetes.resourcepool.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
></kubernetes-events-datatable>
<resource-events-datatable namespace="ctrl.pool.Namespace.Name" storage-key="'kubernetes.resourcepool.events'"> </resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.pool.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading class="vertical-center"><pr-icon icon="'code'"></pr-icon> YAML </uib-tab-heading>

@ -156,17 +156,12 @@
{{ ctrl.state.eventWarningCount }} warning(s)
</div>
</uib-tab-heading>
<kubernetes-events-datatable
title-text="Events"
title-icon="file-text"
dataset="ctrl.events"
table-key="kubernetes.volume.events"
order-by="Date"
reverse-order="true"
loading="ctrl.state.eventsLoading"
refresh-callback="ctrl.getEvents"
<resource-events-datatable
namespace="ctrl.volume.ResourcePool.Namespace.Name"
storage-key="'kubernetes.volume.events'"
resource-id="ctrl.volume.PersistentVolumeClaim.Id"
>
</kubernetes-events-datatable>
</resource-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.volume.PersistentVolumeClaim.Yaml" select="ctrl.showEditor()" classes="btn-sm">

@ -8,7 +8,7 @@ import { Tab, WidgetTabs, findSelectedTabIndex } from '@@/Widget/WidgetTabs';
import { Icon } from '@@/Icon';
import { Badge } from '@@/Badge';
import { EventsDatatable } from '../../components/KubernetesEventsDatatable';
import { EventsDatatable } from '../../components/EventsDatatable';
import {
PlacementsDatatable,

@ -2,6 +2,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
import { useMemo } from 'react';
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { useTableState } from '@@/datatables/useTableState';
@ -10,9 +11,9 @@ import {
useApplicationPods,
useApplicationServices,
} from '../application.queries';
import { EventsDatatable } from '../../components/KubernetesEventsDatatable';
import { useNamespaceEventsQuery } from './useNamespaceEventsQuery';
import { EventsDatatable } from '../../components/EventsDatatable';
import { useEvents } from '../../queries/useEvents';
import { AppKind } from '../types';
const storageKey = 'k8sAppEventsDatatable';
const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
@ -23,41 +24,73 @@ export function ApplicationEventsDatatable() {
params: {
namespace,
name,
'resource-type': resourceType,
'resource-type': appKind,
endpointId: environmentId,
},
} = useCurrentStateAndParams();
const { data: application, ...applicationQuery } = useApplication(
const { relatedEvents, isInitialLoading } = useApplicationEvents(
environmentId,
namespace,
name,
resourceType
appKind,
{
autoRefreshRate: tableState.autoRefreshRate,
}
);
const { data: services, ...servicesQuery } = useApplicationServices(
return (
<EventsDatatable
dataset={relatedEvents}
tableState={tableState}
isLoading={isInitialLoading}
data-cy="k8sAppDetail-eventsTable"
noWidget
/>
);
}
export function useApplicationEvents(
environmentId: EnvironmentId,
namespace: string,
name: string,
appKind?: AppKind,
options?: { autoRefreshRate?: number; yaml?: boolean }
) {
const { data: application, ...applicationQuery } = useApplication(
environmentId,
namespace,
name,
application
appKind
);
const { data: pods, ...podsQuery } = useApplicationPods(
const servicesQuery = useApplicationServices(
environmentId,
namespace,
name,
application
);
const { data: events, ...eventsQuery } = useNamespaceEventsQuery(
const podsQuery = useApplicationPods(
environmentId,
namespace,
{
autoRefreshRate: tableState.autoRefreshRate * 1000,
}
name,
application
);
const { data: events, ...eventsQuery } = useEvents(environmentId, {
namespace,
queryOptions: {
autoRefreshRate: options?.autoRefreshRate
? options.autoRefreshRate * 1000
: undefined,
},
});
// related events are events that have the application id, or the id of a service or pod from the application
const relatedEvents = useMemo(() => {
const serviceIds = services?.map((service) => service?.metadata?.uid);
const podIds = pods?.map((pod) => pod?.metadata?.uid);
const serviceIds = servicesQuery.data?.map(
(service) => service?.metadata?.uid
);
const podIds = podsQuery.data?.map((pod) => pod?.metadata?.uid);
return (
events?.filter(
(event) =>
@ -66,20 +99,13 @@ export function ApplicationEventsDatatable() {
podIds?.includes(event.involvedObject.uid)
) || []
);
}, [application?.metadata?.uid, events, pods, services]);
}, [application?.metadata?.uid, events, podsQuery.data, servicesQuery.data]);
return (
<EventsDatatable
dataset={relatedEvents}
tableState={tableState}
isLoading={
applicationQuery.isLoading ||
eventsQuery.isLoading ||
servicesQuery.isLoading ||
podsQuery.isLoading
}
data-cy="k8sAppDetail-eventsTable"
noWidget
/>
);
const isInitialLoading =
applicationQuery.isInitialLoading ||
servicesQuery.isInitialLoading ||
podsQuery.isInitialLoading ||
eventsQuery.isInitialLoading;
return { relatedEvents, isInitialLoading };
}

@ -10,8 +10,7 @@ import {
useApplicationPods,
useApplicationServices,
} from '../application.queries';
import { useNamespaceEventsQuery } from './useNamespaceEventsQuery';
import { useEvents } from '../../queries/useEvents';
const storageKey = 'k8sAppEventsDatatable';
const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
@ -49,13 +48,12 @@ export function useApplicationEventsTableData() {
name,
application
);
const { data: events, ...eventsQuery } = useNamespaceEventsQuery(
environmentId,
const { data: events, ...eventsQuery } = useEvents(environmentId, {
namespace,
{
queryOptions: {
autoRefreshRate: appEventsTableState.autoRefreshRate * 1000,
}
);
},
});
// related events are events that have the application id, or the id of a service or pod from the application
const appEventsData = useMemo(() => {

@ -1,53 +0,0 @@
import { EventList } from 'kubernetes-types/core/v1';
import { useQuery } from '@tanstack/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
import { parseKubernetesAxiosError } from '../../axiosError';
async function getNamespaceEvents(
environmentId: EnvironmentId,
namespace: string,
labelSelector?: string
) {
try {
const { data } = await axios.get<EventList>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`,
{
params: {
labelSelector,
},
}
);
return data.items;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
}
}
export function useNamespaceEventsQuery(
environmentId: EnvironmentId,
namespace: string,
options?: { autoRefreshRate?: number },
labelSelector?: string
) {
return useQuery(
[
'environments',
environmentId,
'kubernetes',
'events',
namespace,
labelSelector,
],
() => getNamespaceEvents(environmentId, namespace, labelSelector),
{
...withError('Unable to retrieve events'),
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
}
);
}

@ -0,0 +1,53 @@
import { useCurrentStateAndParams } from '@uirouter/react';
import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { useEvents } from '@/react/kubernetes/queries/useEvents';
import { EventsDatatable } from '@/react/kubernetes/components/EventsDatatable';
type Props = {
storageKey: string;
/** if undefined, all resources for the namespace (or cluster are returned) */
resourceId?: string;
/** if undefined, events are fetched for the cluster */
namespace?: string;
};
/** ResourceEventsDatatable returns the EventsDatatable for all events that relate to a specific resource id */
export function ResourceEventsDatatable({
storageKey,
resourceId,
namespace,
}: Props) {
const tableState = useKubeStore(storageKey, {
id: 'Date',
desc: true,
});
const {
params: { endpointId },
} = useCurrentStateAndParams();
const params = resourceId
? { fieldSelector: `involvedObject.uid=${resourceId}` }
: {};
const resourceEventsQuery = useEvents(endpointId, {
namespace,
params,
queryOptions: {
autoRefreshRate: tableState.autoRefreshRate
? tableState.autoRefreshRate * 1000
: undefined,
},
});
const nodeEvents = resourceEventsQuery.data || [];
return (
<EventsDatatable
dataset={nodeEvents}
tableState={tableState}
isLoading={resourceEventsQuery.isLoading}
data-cy="k8sNodeDetail-eventsTable"
noWidget
/>
);
}

@ -0,0 +1,5 @@
/** Kubernetes environment base query keys */
export const queryKeys = {
base: (environmentId: number) =>
['environments', environmentId, 'kubernetes'] as const,
};

@ -0,0 +1,86 @@
import { EventList } from 'kubernetes-types/core/v1';
import { useQuery } from '@tanstack/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { withError } from '@/react-tools/react-query';
import { parseKubernetesAxiosError } from '../axiosError';
import { queryKeys as environmentQueryKeys } from './query-keys';
type RequestOptions = {
/** if undefined, events are fetched at the cluster scope */
namespace?: string;
params?: {
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors */
labelSelector?: string;
/** https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors */
fieldSelector?: string;
};
};
const queryKeys = {
base: (environmentId: number, { namespace, params }: RequestOptions) => {
if (namespace) {
return [
...environmentQueryKeys.base(environmentId),
'events',
namespace,
params,
] as const;
}
return [
...environmentQueryKeys.base(environmentId),
'events',
params,
] as const;
},
};
async function getEvents(
environmentId: EnvironmentId,
options?: RequestOptions
) {
const { namespace, params } = options ?? {};
try {
const { data } = await axios.get<EventList>(
buildUrl(environmentId, namespace),
{
params,
}
);
return data.items;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve events');
}
}
type QueryOptions = {
queryOptions?: {
autoRefreshRate?: number;
};
} & RequestOptions;
export function useEvents(
environmentId: EnvironmentId,
options?: QueryOptions
) {
const { queryOptions, params, namespace } = options ?? {};
return useQuery(
queryKeys.base(environmentId, { params, namespace }),
() => getEvents(environmentId, { params, namespace }),
{
...withError('Unable to retrieve events'),
refetchInterval() {
return queryOptions?.autoRefreshRate ?? false;
},
}
);
}
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
return namespace
? `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/events`
: `/endpoints/${environmentId}/kubernetes/api/v1/events`;
}
Loading…
Cancel
Save