Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add users list to vm details page #5698

Merged
merged 1 commit into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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];