-
Notifications
You must be signed in to change notification settings - Fork 465
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For #23912 new UI for activities on the global, past, and upcoming feeds. These are the same changes in [this PR](#25329), except we are reverting the changes around fleet initiated activities as that is not in the current activities API. We are doing this so that the new activities can go out in a release while the backend is still being built and will be ready later. > NOTE: this does contain the code for cancel activity functionality but it hidden from the user. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
- Loading branch information
1 parent
3f98d25
commit cdefa0c
Showing
42 changed files
with
922 additions
and
770 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
- update the UI a new activities design |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import React from "react"; | ||
import ReactTooltip from "react-tooltip"; | ||
import classnames from "classnames"; | ||
|
||
import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; | ||
import { | ||
addGravatarUrlToResource, | ||
internationalTimeFormat, | ||
} from "utilities/helpers"; | ||
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants"; | ||
|
||
import Avatar from "components/Avatar"; | ||
|
||
import { COLORS } from "styles/var/colors"; | ||
import { dateAgo } from "utilities/date_format"; | ||
import Button from "components/buttons/Button"; | ||
import Icon from "components/Icon"; | ||
import { noop } from "lodash"; | ||
|
||
const baseClass = "activity-item"; | ||
|
||
export interface IShowActivityDetailsData { | ||
type: string; | ||
details?: IActivityDetails; | ||
} | ||
|
||
/** | ||
* A handler that will show the details of an activity. This is used to pass | ||
* the details of an activity to the parent component to show the details of | ||
* the activity. | ||
*/ | ||
export type ShowActivityDetailsHandler = ({ | ||
type, | ||
details, | ||
}: IShowActivityDetailsData) => void; | ||
|
||
interface IActivityItemProps { | ||
activity: IActivity; | ||
children: React.ReactNode; | ||
/** | ||
* Set this to `true` when rendering only this activity by itself. This will | ||
* change the styles for the activity item for solo rendering. | ||
* @default false */ | ||
isSoloActivity?: boolean; | ||
/** | ||
* Set this to `true` to hide the show details button and prevent from rendering. | ||
* Not all activities can show details, so this is a way to hide the button. | ||
* @default false | ||
*/ | ||
hideShowDetails?: boolean; | ||
/** | ||
* Set this to `true` to hide the close button and prevent from rendering | ||
* @default false | ||
*/ | ||
hideCancel?: boolean; | ||
/** | ||
* Set this to `true` to disable the cancel button. It will still render but | ||
* will not be clickable. | ||
* @default false | ||
*/ | ||
disableCancel?: boolean; | ||
className?: string; | ||
onShowDetails?: ShowActivityDetailsHandler; | ||
onCancel?: () => void; | ||
} | ||
|
||
/** | ||
* A wrapper that will render all the common elements of a host activity item. | ||
* This includes the avatar, the created at timestamp, and a dash to separate | ||
* the activity items. The `children` will be the specific details of the activity | ||
* implemented in the component that uses this wrapper. | ||
*/ | ||
const ActivityItem = ({ | ||
activity, | ||
children, | ||
className, | ||
isSoloActivity, | ||
hideShowDetails = false, | ||
hideCancel = false, | ||
disableCancel = false, | ||
onShowDetails = noop, | ||
onCancel = noop, | ||
}: IActivityItemProps) => { | ||
const { actor_email } = activity; | ||
const { gravatar_url } = actor_email | ||
? addGravatarUrlToResource({ email: actor_email }) | ||
: { gravatar_url: DEFAULT_GRAVATAR_LINK }; | ||
|
||
// wrapped just in case the date string does not parse correctly | ||
let activityCreatedAt: Date | null = null; | ||
try { | ||
activityCreatedAt = new Date(activity.created_at); | ||
} catch (e) { | ||
activityCreatedAt = null; | ||
} | ||
|
||
const classNames = classnames(baseClass, className, { | ||
[`${baseClass}__solo-activity`]: isSoloActivity, | ||
[`${baseClass}__no-details`]: hideShowDetails, | ||
}); | ||
|
||
const onShowActivityDetails = () => { | ||
onShowDetails({ type: activity.type, details: activity.details }); | ||
}; | ||
|
||
const onCancelActivity = (e: React.MouseEvent<HTMLButtonElement>) => { | ||
e.stopPropagation(); | ||
onCancel(); | ||
}; | ||
|
||
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in | ||
// the backend. For now, if all these fields are empty, then we assume it was | ||
// Fleet-initiated. | ||
let fleetInitiated = false; | ||
if ( | ||
!activity.actor_email && | ||
!activity.actor_full_name && | ||
(activity.type === ActivityType.InstalledSoftware || | ||
activity.type === ActivityType.InstalledAppStoreApp || | ||
activity.type === ActivityType.RanScript) | ||
) { | ||
fleetInitiated = true; | ||
} | ||
|
||
return ( | ||
<div className={classNames}> | ||
<div className={`${baseClass}__avatar-wrapper`}> | ||
<div className={`${baseClass}__avatar-upper-dash`} /> | ||
<Avatar | ||
className={`${baseClass}__avatar-image`} | ||
user={{ gravatar_url }} | ||
size="small" | ||
hasWhiteBackground | ||
useFleetAvatar={fleetInitiated} | ||
/> | ||
<div className={`${baseClass}__avatar-lower-dash`} /> | ||
</div> | ||
<div | ||
className={`${baseClass}__details-wrapper`} | ||
onClick={onShowActivityDetails} | ||
> | ||
<div className={"activity-details"}> | ||
<span className={`${baseClass}__details-topline`}> | ||
<span>{children}</span> | ||
</span> | ||
<br /> | ||
<span | ||
className={`${baseClass}__details-bottomline`} | ||
data-tip | ||
data-for={`activity-${activity.id}`} | ||
> | ||
{activityCreatedAt && dateAgo(activityCreatedAt)} | ||
</span> | ||
{activityCreatedAt && ( | ||
<ReactTooltip | ||
className="date-tooltip" | ||
place="top" | ||
type="dark" | ||
effect="solid" | ||
id={`activity-${activity.id}`} | ||
backgroundColor={COLORS["tooltip-bg"]} | ||
> | ||
{internationalTimeFormat(activityCreatedAt)} | ||
</ReactTooltip> | ||
)} | ||
</div> | ||
<div className={`${baseClass}__details-actions`}> | ||
{!hideShowDetails && ( | ||
<Button variant="icon" onClick={onShowActivityDetails}> | ||
<Icon name="info-outline" /> | ||
</Button> | ||
)} | ||
{!hideCancel && ( | ||
<Button | ||
variant="icon" | ||
onClick={onCancelActivity} | ||
disabled={disableCancel} | ||
> | ||
<Icon | ||
name="close" | ||
color="ui-fleet-black-75" | ||
className={`${baseClass}__close-icon`} | ||
/> | ||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ActivityItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
.activity-item { | ||
display: grid; // Grid system is used to create variable solid line lengths | ||
grid-template-columns: 16px 16px 8px 1fr; | ||
grid-template-rows: max-content; | ||
|
||
&__avatar-wrapper { | ||
box-sizing: border-box; | ||
display: grid; | ||
grid-template-columns: 16px 16px; | ||
grid-template-rows: 8px 32px 1fr; | ||
grid-column-start: 1; | ||
grid-column-end: 3; | ||
|
||
.avatar-wrapper { | ||
grid-column-start: 1; | ||
grid-column-end: 3; | ||
grid-row-start: 2; | ||
grid-row-end: 3; | ||
} | ||
} | ||
|
||
&__avatar-upper-dash { | ||
border-right: 1px solid $ui-fleet-black-10; | ||
grid-column-start: 1; | ||
grid-column-end: 2; | ||
grid-row-start: 1; | ||
grid-row-end: 2; | ||
} | ||
|
||
&__avatar-lower-dash { | ||
border-right: 1px solid $ui-fleet-black-10; | ||
grid-column-start: 1; | ||
grid-column-end: 2; | ||
grid-row-start: 3; | ||
grid-row-end: 4; | ||
} | ||
|
||
&__details-wrapper { | ||
display: flex; | ||
gap: $pad-medium; | ||
justify-content: space-between; | ||
align-items: center; | ||
grid-column-start: 4; | ||
grid-row-start: 1; | ||
padding: $pad-small; | ||
margin-bottom: $pad-large; | ||
|
||
.premium-icon-tip { | ||
position: relative; | ||
top: 4px; | ||
padding-right: $pad-xsmall; | ||
} | ||
|
||
.activity-details { | ||
margin: 0; | ||
line-height: 16px; | ||
} | ||
|
||
&:hover { | ||
border-radius: $border-radius-large; | ||
background-color: $ui-off-white; | ||
cursor: pointer; | ||
|
||
.activity-item__details-actions { | ||
visibility: visible; | ||
} | ||
} | ||
|
||
.button { | ||
height: 16px; | ||
|
||
&--icon svg { | ||
padding: 0; | ||
} | ||
} | ||
} | ||
|
||
&__details-actions { | ||
visibility: hidden; | ||
display: flex; | ||
gap: $pad-medium; | ||
} | ||
|
||
&__close-icon { | ||
cursor: pointer; | ||
&:hover { | ||
svg { | ||
path { | ||
stroke: $core-vibrant-blue; | ||
} | ||
} | ||
} | ||
}; | ||
|
||
&__details-topline { | ||
font-size: $x-small; | ||
overflow-wrap: anywhere; | ||
} | ||
|
||
&__details-content { | ||
margin-right: $pad-xsmall; | ||
} | ||
|
||
&__details-bottomline { | ||
font-size: $xx-small; | ||
color: $ui-fleet-black-50; | ||
} | ||
|
||
&__show-query-icon { | ||
margin-left: $pad-xsmall; | ||
} | ||
|
||
&:first-child { | ||
.activity-item__avatar-upper-dash { | ||
border-right: none; | ||
} | ||
} | ||
|
||
&:last-child { | ||
.activity-item__avatar-lower-dash { | ||
border-right: none; | ||
} | ||
} | ||
|
||
/** | ||
* Starting here are the styles for the activity item when it is the | ||
* only activity that is being displayed (controlled by the `soloActivity prop`. | ||
* We switch from grid to flexbox since we don't need the solid lines anymore. | ||
* we also dont show to actions on hover | ||
*/ | ||
&__solo-activity { | ||
border: 1px solid $ui-fleet-black-10; | ||
border-radius: $border-radius-large; | ||
padding: $pad-medium; | ||
display: flex; | ||
gap: $pad-medium; | ||
|
||
.activity-item__avatar-wrapper { | ||
display: block; | ||
} | ||
|
||
.activity-item__avatar-lower-dash { | ||
display: none; | ||
} | ||
|
||
.activity-item__details-wrapper { | ||
display: block; | ||
padding: 0; | ||
margin-bottom: 0; | ||
|
||
&:hover { | ||
cursor: auto; | ||
background-color: transparent; | ||
} | ||
} | ||
|
||
.activity-item__details-actions { | ||
display: none; | ||
} | ||
} | ||
|
||
&__no-details { | ||
.activity-item__details-wrapper { | ||
&:hover { | ||
cursor: auto; | ||
background-color: transparent; | ||
} | ||
} | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from "react"; | ||
import { render, screen } from "@testing-library/react"; | ||
|
||
import Avatar from "./Avatar"; | ||
|
||
describe("Avatar - component", () => { | ||
it("renders the user gravatar if provided", () => { | ||
render( | ||
<Avatar user={{ gravatar_url: "https://example.com/avatar.png" }} /> | ||
); | ||
|
||
const avatar = screen.getByAltText("User avatar"); | ||
expect(avatar).toBeVisible(); | ||
expect(avatar).toHaveAttribute("src", "https://example.com/avatar.png"); | ||
}); | ||
|
||
it("renders the fleet avatar if useFleetAvatar is `true`", () => { | ||
render(<Avatar useFleetAvatar user={{}} />); | ||
expect(screen.getByTestId("fleet-avatar")).toBeVisible(); | ||
}); | ||
}); |
Oops, something went wrong.