Skip to content

Commit

Permalink
add users list to vm details page
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacov committed Jun 10, 2020
1 parent 5124c9f commit 315c8c4
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import * as React from 'react';
import AlertsBody from '@console/shared/src/components/dashboard/status-card/AlertsBody';
import { StatusItem } from '@console/shared/src/components/dashboard/status-card/AlertItem';
import { BlueInfoCircleIcon } from '@console/shared/src/components/status';
import { NO_GUEST_AGENT_MESSAGE } from '../../../constants/vm/constants';
import { VMIKind } from '../../../types';
import { getVMIConditionsByType } from '../../../selectors/vmi';

// Based on: https://github.com/kubevirt/kubevirt/blob/f71e9c9615a6c36178169d66814586a93ba515b5/staging/src/kubevirt.io/client-go/api/v1/types.go#L337
const VMI_CONDITION_AGENT_CONNECTED = 'AgentConnected';

const isGuestAgentInstalled = (vmi: VMIKind) => {
export const isGuestAgentInstalled = (vmi: VMIKind) => {
// the condition type is unique
const conditions = getVMIConditionsByType(vmi, VMI_CONDITION_AGENT_CONNECTED);
return conditions && conditions.length > 0 && conditions[0].status === 'True';
Expand All @@ -17,10 +18,7 @@ const isGuestAgentInstalled = (vmi: VMIKind) => {
export const VMAlerts: React.FC<VMAlertsProps> = ({ vmi }) => (
<AlertsBody>
{vmi && vmi.status && !isGuestAgentInstalled(vmi) && (
<StatusItem
Icon={BlueInfoCircleIcon}
message="This VM does not have guest agent installed. Some metrics and management features will not be available."
/>
<StatusItem Icon={BlueInfoCircleIcon} message={NO_GUEST_AGENT_MESSAGE} />
)}
</AlertsBody>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getLoadedData, getResource } from '../../utils';
import { VirtualMachineInstanceModel, VirtualMachineModel } from '../../models';
import { getServicesForVmi } from '../../selectors/service';
import { VMResourceSummary, VMDetailsList, VMSchedulingList } from './vm-resource';
import { VMUsersList } from './vm-users';
import { VMTabProps } from './types';
import { getVMStatus } from '../../statuses/vm/vm-status';
import { VMStatusBundle } from '../../statuses/vm/types';
Expand Down Expand Up @@ -115,6 +116,10 @@ export const VMDetails: React.FC<VMDetailsProps> = (props) => {
<SectionHeading text="Services" />
<ServicesList {...restProps} data={vmServicesData} label="Services" />
</div>
<div className="co-m-pane__body">
<SectionHeading text="Logged in users" />
<VMUsersList {...restProps} vmi={vmi} vmStatusBundle={vmStatusBundle} />
</div>
</StatusBox>
);
};
Expand Down
136 changes: 136 additions & 0 deletions frontend/packages/kubevirt-plugin/src/components/vms/vm-users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { sortable } from '@patternfly/react-table';
import { fromNow } from '@console/internal/components/utils/datetime';
import { Table, TableRow, TableData } from '@console/internal/components/factory';
import { Timestamp } from '@console/internal/components/utils/timestamp';
import {
useURLPoll,
URL_POLL_DEFAULT_DELAY,
} from '@console/internal/components/utils/url-poll-hook';
import { StatusItem } from '@console/shared/src/components/dashboard/status-card/AlertItem';
import { BlueInfoCircleIcon } from '@console/shared/src/components/status';
import {
VIRTUAL_MACHINE_IS_NOT_RUNNING,
NO_GUEST_AGENT_MESSAGE,
} from '../../constants/vm/constants';
import { VMStatus } from '../../constants/vm/vm-status';
import { isGuestAgentInstalled } from '../dashboards-page/vm-dashboard/vm-alerts';
import { VMStatusBundle } from '../../statuses/vm/types';
import { VMIKind } from '../../types';
import { getVMIApiPath, getVMISubresourcePath } from '../../selectors/vmi/selectors';

const guestAgentURL = (vmi: VMIKind) =>
vmi &&
isGuestAgentInstalled(vmi) &&
`/${getVMISubresourcePath()}/${getVMIApiPath(vmi)}/guestosinfo`;

const tableColumnClasses = [
classNames('col-lg-3', 'col-md-3', 'col-sm-4', 'col-sm-4'),
classNames('col-lg-3', 'col-md-3', 'col-sm-4', 'col-sm-4'),
classNames('col-lg-3', 'col-md-3', 'col-sm-4', 'col-sm-4'),
classNames('col-lg-3', 'col-md-3', 'hidden-sm', 'hidden-xs'),
];

const UsersTableHeader = () => {
return [
{
title: 'User Name',
sortField: 'metadata.userName',
transforms: [sortable],
props: { className: tableColumnClasses[0] },
},
{
title: 'Domain',
sortField: 'metadata.domain',
transforms: [sortable],
props: { className: tableColumnClasses[1] },
},
{
title: 'Login time',
sortField: 'metadata.loginTime',
transforms: [sortable],
props: { className: tableColumnClasses[2] },
},
{
title: 'Elapsed logged in time',
props: { className: tableColumnClasses[3] },
},
];
};
UsersTableHeader.displayName = 'UsersTableHeader';

const UsersTableRow = ({ obj: user, index, key, style }) => {
return (
<TableRow id={user?.metadata?.uid} index={index} trKey={key} style={style}>
<TableData className={tableColumnClasses[0]}>{user?.metadata?.userName}</TableData>
<TableData className={classNames(tableColumnClasses[1], 'co-break-word')}>
{user?.metadata?.domain}
</TableData>
<TableData className={tableColumnClasses[2]}>
<Timestamp timestamp={new Date(user?.metadata?.loginTime).toUTCString()} />
</TableData>
<TableData className={tableColumnClasses[3]}>{fromNow(user?.metadata?.loginTime)}</TableData>
</TableRow>
);
};

export const VMUsersList: React.FC<VMUsersListProps> = ({
vmi,
vmStatusBundle,
delay = URL_POLL_DEFAULT_DELAY,
}) => {
const [response, error, loading] = useURLPoll<VirtualMachineInstanceGuestAgentInfo>(
guestAgentURL(vmi),
delay,
);

if (vmStatusBundle.status !== VMStatus.RUNNING) {
return <div className="text-center">{VIRTUAL_MACHINE_IS_NOT_RUNNING}</div>;
}

if (!isGuestAgentInstalled(vmi)) {
return <StatusItem Icon={BlueInfoCircleIcon} message={NO_GUEST_AGENT_MESSAGE} />;
}

const data =
response &&
response?.userList &&
response?.userList.map((user, uid) => ({
metadata: {
uid,
userName: user?.userName,
domain: user?.domain,
loginTime: user?.loginTime && user.loginTime * 1000,
},
}));

return (
<Table
aria-label="Users"
Header={UsersTableHeader}
Row={UsersTableRow}
data={data}
loadError={error?.message}
loaded={!loading}
EmptyMsg={() => <div className="text-center">No Logged In Users</div>}
virtualize
/>
);
};

type VirtualMachineInstanceGuestOSUser = {
userName: string;
domain?: string;
loginTime?: number;
};

type VirtualMachineInstanceGuestAgentInfo = {
userList?: VirtualMachineInstanceGuestOSUser[];
};

type VMUsersListProps = {
vmi?: VMIKind;
vmStatusBundle?: VMStatusBundle;
delay?: number;
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ export const VM_DETAIL_EVENTS_HREF = 'events';

export const PAUSED_VM_MODAL_MESSAGE =
'This VM has been paused. If you wish to unpause it, please click the Unpause button below. For further details, please check with your system administrator.';

export const VIRTUAL_MACHINE_IS_NOT_RUNNING = 'Virtual Machine is not running';
export const NO_GUEST_AGENT_MESSAGE =
'This VM does not have guest agent installed. Some metrics and management features will not be available';
36 changes: 3 additions & 33 deletions frontend/public/components/graphs/prometheus-poll-hook.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useState } from 'react';

import { usePoll, useSafeFetch } from '../utils';
import { useURLPoll } from '../utils/url-poll-hook';
import { getPrometheusURL, PrometheusEndpoint } from './helpers';
import { PrometheusResponse } from '.';

const DEFAULT_DELAY = 15000; // 15 seconds
const DEFAULT_SAMPLES = 60;
const DEFAULT_TIMESPAN = 60 * 60 * 1000; // 1 hour

export const usePrometheusPoll = ({
delay = DEFAULT_DELAY,
delay,
endpoint,
endTime = undefined,
namespace,
Expand All @@ -20,34 +16,8 @@ export const usePrometheusPoll = ({
timespan = DEFAULT_TIMESPAN,
}: PrometheusPollProps) => {
const url = getPrometheusURL({ endpoint, endTime, namespace, query, samples, timeout, timespan });
const [error, setError] = useState();
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const safeFetch = useSafeFetch();
const tick = useCallback(() => {
if (url) {
safeFetch(url)
.then((data) => {
setResponse(data);
setError(undefined);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
// eslint-disable-next-line no-console
console.error(`Error polling Prometheus: ${err}`);
}
});
} else {
setLoading(false);
}
}, [url]);

usePoll(tick, delay, endTime, query, timespan);

return [response, error, loading] as [PrometheusResponse, Error, boolean];
return useURLPoll<PrometheusResponse>(url, delay, endTime, query, timespan);
};

type PrometheusPollProps = {
Expand Down
43 changes: 43 additions & 0 deletions frontend/public/components/utils/url-poll-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useState } from 'react';
import { usePoll } from './poll-hook';
import { useSafeFetch } from './safe-fetch-hook';

export const URL_POLL_DEFAULT_DELAY = 15000; // 15 seconds

export const useURLPoll = <R>(
url: string,
delay = URL_POLL_DEFAULT_DELAY,
...dependencies: any[]
): URLPoll<R> => {
const [error, setError] = useState();
const [response, setResponse] = useState<R>();
const [loading, setLoading] = useState(true);
const safeFetch = useSafeFetch();
const tick = useCallback(() => {
if (url) {
safeFetch(url)
.then((data) => {
setResponse(data);
setError(undefined);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
// eslint-disable-next-line no-console
console.error(`Error polling URL: ${err}`);
}
});
} else {
setLoading(false);
}
}, [url]);

usePoll(tick, delay, ...dependencies);

return [response, error, loading];
};

export type URLPoll<R> = [R, any, boolean];

0 comments on commit 315c8c4

Please sign in to comment.