Add query author avatar; format last modified time (#2898)

Co-authored-by: Luke Heath <luke@fleetdm.com>
This commit is contained in:
gillespi314
2021-11-12 16:45:53 -06:00
committed by GitHub
co-authored by Luke Heath
parent 8c0c00884f
commit 86687a80cc
12 changed files with 178 additions and 47 deletions
+2
View File
@@ -0,0 +1,2 @@
* Add author avatars to manage queries page and edit query form
* Format query last modified time in words
+4 -3
View File
@@ -1,6 +1,8 @@
import React, { useState, useCallback } from "react";
import classnames from "classnames";
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
interface IAvatarUserInterface {
gravatarURL: string;
}
@@ -24,9 +26,8 @@ const Avatar = ({ className, size, user }: IAvatarInterface): JSX.Element => {
setIsError(true);
}, []);
const isSmall = size !== undefined && size.toLowerCase() === "small";
const avatarClasses = classnames(baseClass, className, {
[`${baseClass}--${size}`]: isSmall,
[`${baseClass}--${size?.toLowerCase()}`]: !!size,
});
const { gravatarURL } = user;
@@ -35,7 +36,7 @@ const Avatar = ({ className, size, user }: IAvatarInterface): JSX.Element => {
<img
alt={!isLoading && !isError ? "User avatar" : ""}
className={`${avatarClasses} ${isLoading || isError ? "default" : ""}`}
src={gravatarURL}
src={gravatarURL || DEFAULT_GRAVATAR_LINK}
onError={onError}
onLoad={onLoad}
/>
+4
View File
@@ -8,6 +8,10 @@
@include size(32px);
}
&--xsmall {
@include size(16px);
}
img {
&.default {
display: none;
@@ -435,7 +435,14 @@ const DataTable = ({
>
{row.cells.map((cell: any) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
<td
className={
cell.column.id ? `${cell.column.id}__cell` : ""
}
{...cell.getCellProps()}
>
{cell.render("Cell")}
</td>
);
})}
</tr>
+1
View File
@@ -36,6 +36,7 @@ export interface IQuery {
saved: boolean;
author_id: number;
author_name: string;
author_email: string;
observer_can_run: boolean;
packs: IPack[];
stats?: IScheduledQueryStats;
@@ -109,7 +109,7 @@ const ManageQueriesPage = (): JSX.Element => {
{
// refetchOnMount: false,
// refetchOnReconnect: false,
// refetchOnWindowFocus: false,
refetchOnWindowFocus: false,
select: (data: IFleetQueriesResponse) => data.queries,
}
);
@@ -182,4 +182,15 @@
}
}
}
.author_name__cell {
img,
div,
span {
display: flex;
align-items: center;
}
div {
padding-right: $pad-small;
}
}
}
@@ -3,11 +3,12 @@
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import ReactTooltip from "react-tooltip";
import format from "date-fns/format";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import permissionsUtils from "utilities/permissions";
// @ts-ignore
import Avatar from "components/Avatar";
import Checkbox from "components/forms/fields/Checkbox";
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
@@ -19,6 +20,7 @@ import PATHS from "router/paths";
import { IQuery } from "interfaces/query";
import { IUser } from "interfaces/user";
import { addGravatarUrlToResource } from "fleet/helpers";
interface IQueryRow {
id: string;
@@ -112,9 +114,19 @@ const generateTableHeaders = (currentUser: IUser): IDataColumn[] => {
/>
),
accessor: "author_name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
Cell: (cellProps: ICellProps): JSX.Element => {
const { author_name, author_email } = cellProps.row.original;
const author = author_name === currentUser.name ? "You" : author_name;
return (
<span>
<Avatar
user={addGravatarUrlToResource({ email: author_email })}
size="xsmall"
/>
{author}
</span>
);
},
sortType: "caseInsensitive",
},
{
@@ -127,7 +139,12 @@ const generateTableHeaders = (currentUser: IUser): IDataColumn[] => {
),
accessor: "updated_at",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={format(new Date(cellProps.cell.value), "MM/dd/yy")} />
<TextCell
value={formatDistanceToNow(new Date(cellProps.cell.value), {
includeSeconds: true,
addSuffix: true,
})}
/>
),
},
];
@@ -4,6 +4,7 @@ import ReactTooltip from "react-tooltip";
import { size } from "lodash";
import { useDebouncedCallback } from "use-debounce/lib";
import { addGravatarUrlToResource } from "fleet/helpers";
// @ts-ignore
import { listCompatiblePlatforms, parseSqlTables } from "utilities/sql_tools";
@@ -11,6 +12,7 @@ import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { IQuery, IQueryFormData } from "interfaces/query";
import Avatar from "components/Avatar";
import FleetAce from "components/FleetAce"; // @ts-ignore
import validateQuery from "components/forms/validators/validate_query";
import Button from "components/buttons/Button";
@@ -169,6 +171,27 @@ const QueryForm = ({
}
};
const renderAuthor = (): JSX.Element | null => {
return storedQuery ? (
<>
<b>Author</b>
<div>
<Avatar
user={addGravatarUrlToResource({
email: storedQuery.author_email,
})}
size="xsmall"
/>
<span>
{storedQuery.author_name === currentUser?.name
? "You"
: storedQuery.author_name}
</span>
</div>
</>
) : null;
};
const renderLabelComponent = (): JSX.Element | null => {
if (!showOpenSchemaActionText) {
return null;
@@ -261,12 +284,17 @@ const QueryForm = ({
const renderRunForObserver = (
<form className={`${baseClass}__wrapper`}>
<h1 className={`${baseClass}__query-name no-hover`}>
{lastEditedQueryName}
</h1>
<p className={`${baseClass}__query-description no-hover`}>
{lastEditedQueryDescription}
</p>
<div className={`${baseClass}__title-bar`}>
<div className="name-description">
<h1 className={`${baseClass}__query-name no-hover`}>
{lastEditedQueryName}
</h1>
<p className={`${baseClass}__query-description no-hover`}>
{lastEditedQueryDescription}
</p>
</div>
<div className="author">{renderAuthor()}</div>
</div>
<Button
className={`${baseClass}__toggle-sql`}
variant="text-link"
@@ -303,31 +331,36 @@ const QueryForm = ({
const renderForGlobalAdminOrAnyMaintainer = (
<>
<form className={`${baseClass}__wrapper`} autoComplete="off">
{isEditMode ? (
<InputField
id="query-name"
type="text"
name="query-name"
error={errors.name}
value={lastEditedQueryName}
placeholder="Add name here"
inputClassName={`${baseClass}__query-name`}
onChange={setLastEditedQueryName}
/>
) : (
<h1 className={`${baseClass}__query-name no-hover`}>New query</h1>
)}
{isEditMode && (
<InputField
id="query-description"
type="text"
name="query-description"
value={lastEditedQueryDescription}
placeholder="Add description here."
inputClassName={`${baseClass}__query-description`}
onChange={setLastEditedQueryDescription}
/>
)}
<div className={`${baseClass}__title-bar`}>
<div className="name-description">
{isEditMode ? (
<InputField
id="query-name"
type="textarea"
name="query-name"
error={errors.name}
value={lastEditedQueryName}
placeholder="Add name here"
inputClassName={`${baseClass}__query-name`}
onChange={setLastEditedQueryName}
/>
) : (
<h1 className={`${baseClass}__query-name no-hover`}>New query</h1>
)}
{isEditMode && (
<InputField
id="query-description"
type="textarea"
name="query-description"
value={lastEditedQueryDescription}
placeholder="Add description here."
inputClassName={`${baseClass}__query-description`}
onChange={setLastEditedQueryDescription}
/>
)}
</div>
<div className="author">{isEditMode && renderAuthor()}</div>
</div>
<FleetAce
value={lastEditedQueryBody}
error={errors.query}
@@ -13,6 +13,66 @@
}
}
&__title-bar {
display: flex;
justify-content: space-between;
.form-field {
margin-bottom: 0px;
}
.input-field,
.input-field__text-area {
min-height: auto;
line-height: normal;
white-space: normal;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.input-field::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.input-field {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.name-description {
flex-grow: 1;
margin-right: 24px;
.query-form__query-name {
line-height: 2rem;
height: 2rem;
}
.query-form__query-description {
margin: 0.25rem 0 1rem;
}
}
.author {
flex-shrink: 1;
margin-top: 24px;
text-align: right;
justify-content: right;
b {
text-align: right;
}
img,
div {
display: flex;
align-items: center;
}
span {
padding-left: $pad-small;
}
}
}
&__query-name,
&__query-description {
width: 100%;
@@ -20,13 +80,8 @@
padding: 0;
border: 0;
resize: none;
height: 30px;
white-space: nowrap;
white-space: normal;
background-color: transparent;
&.input-field {
overflow-x: scroll;
}
&:hover:not(.focus-visible):not(.no-hover) {
color: $core-vibrant-blue;
cursor: pointer;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB