Add query author avatar; format last modified time (#2898)
Co-authored-by: Luke Heath <luke@fleetdm.com>
This commit is contained in:
co-authored by
Luke Heath
parent
8c0c00884f
commit
86687a80cc
@@ -0,0 +1,2 @@
|
||||
* Add author avatars to manage queries page and edit query form
|
||||
* Format query last modified time in words
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-5
@@ -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 |
Reference in New Issue
Block a user