diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.mdx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.mdx
new file mode 100644
index 00000000000..5e8ae855dc0
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.mdx
@@ -0,0 +1,69 @@
+import { Props } from '@storybook/addon-docs/blocks';
+import { UserIcon } from './UserIcon';
+
+# UserIcon
+
+`UserIcon` a component that takes in the `UserIconProps` interface as a prop. It renders a user icon and displays the user's name or initials along with the user's active status or last viewed date.
+
+## Usage
+
+To use the `UserIcon` component, import it and pass in the required `UserIconProps`. The component can be used as follows:
+
+```jsx
+import { UserIcon } from '@grafana/ui';
+
+const ExampleComponent = () => {
+ const userView = {
+ user: { id: 1, name: 'John Smith', avatarUrl: 'https://example.com/avatar.png' },
+ lastActiveAt: '2023-04-18T15:00:00.000Z',
+ };
+
+ return (
+
+
+
+ );
+};
+```
+
+### With custom `children`
+
+`children` prop can be used to display a custom content inside `UserIcon`. This is useful to show the data about extra users.
+
+```jsx
+import { UserIcon } from '@grafana/ui';
+
+const ExampleComponent = () => {
+ const userView = {
+ user: { id: 1, name: 'John Smith', avatarUrl: 'https://example.com/avatar.png' },
+ lastActiveAt: '2023-04-18T15:00:00.000Z',
+ };
+
+ return (
+
+
+ +10
+
+
+ );
+};
+```
+
+
+
+## UserView type
+
+```tsx
+import { DateTimeInput } from '@grafana/data';
+
+export interface UserView {
+ user: {
+ /** User's name, containing first + last name */
+ name: string;
+ /** URL to the user's avatar */
+ avatarUrl?: string;
+ };
+ /** Datetime string when the user was last active */
+ lastActiveAt: DateTimeInput;
+}
+```
diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx
new file mode 100644
index 00000000000..368bffb07f0
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import React from 'react';
+
+import { UserIcon } from './UserIcon';
+import mdx from './UserIcon.mdx';
+
+const meta: ComponentMeta = {
+ title: 'General/UsersIndicator/UserIcon',
+ component: UserIcon,
+ argTypes: {},
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ knobs: {
+ disabled: true,
+ },
+ controls: {
+ exclude: ['className', 'onClick'],
+ },
+ actions: {
+ disabled: true,
+ },
+ },
+ args: {
+ showTooltip: false,
+ onClick: undefined,
+ },
+};
+
+export const Basic: ComponentStory = (args) => {
+ const userView = {
+ user: {
+ name: 'John Smith',
+ avatarUrl: 'https://picsum.photos/id/1/200/200',
+ },
+ lastActiveAt: '2023-04-18T15:00:00.000Z',
+ };
+
+ return ;
+};
+Basic.args = {
+ showTooltip: true,
+ onClick: undefined,
+};
+
+export default meta;
diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.test.tsx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.test.tsx
new file mode 100644
index 00000000000..436925ee16a
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import { UserIcon } from './UserIcon';
+
+// setup userEvent
+function setup(jsx: React.ReactElement) {
+ return {
+ user: userEvent.setup(),
+ ...render(jsx),
+ };
+}
+
+const testUserView = {
+ user: {
+ name: 'John Smith',
+ avatarUrl: 'https://example.com/avatar.png',
+ },
+ lastActiveAt: new Date().toISOString(),
+};
+
+describe('UserIcon', () => {
+ it('renders user initials when no avatar URL is provided', () => {
+ render();
+ expect(screen.getByLabelText('John Smith icon')).toHaveTextContent('JS');
+ });
+
+ it('renders avatar when URL is provided', () => {
+ render();
+ expect(screen.getByAltText('John Smith avatar')).toHaveAttribute('src', 'https://example.com/avatar.png');
+ });
+
+ it('calls onClick handler when clicked', async () => {
+ const handleClick = jest.fn();
+ const { user } = setup();
+ await user.click(screen.getByLabelText('John Smith icon'));
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx
new file mode 100644
index 00000000000..13e03a3303a
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx
@@ -0,0 +1,171 @@
+import { css, cx } from '@emotion/css';
+import React, { useMemo, PropsWithChildren } from 'react';
+
+import { dateTime, DateTimeInput, GrafanaTheme2 } from '@grafana/data';
+
+import { useTheme2 } from '../../themes';
+import { Tooltip } from '../Tooltip';
+
+import { UserView } from './types';
+
+export interface UserIconProps {
+ /** An object that contains the user's details and 'lastActiveAt' status */
+ userView: UserView;
+ /** A boolean value that determines whether the tooltip should be shown or not */
+ showTooltip?: boolean;
+ /** An optional class name to be added to the icon element */
+ className?: string;
+ /** onClick handler to be called when the icon is clicked */
+ onClick?: () => void;
+}
+
+/**
+ * A helper function that takes in a dateString parameter
+ * and returns the user's last viewed date in a specific format.
+ */
+const formatViewed = (dateString: DateTimeInput): string => {
+ const date = dateTime(dateString);
+ const diffHours = date.diff(dateTime(), 'hours', false);
+ return `Active last ${(Math.floor(-diffHours / 24) + 1) * 24}h`;
+};
+
+/**
+ * Output the initials of the first and last name (if given), capitalized and concatenated together.
+ * If name is not provided, an empty string is returned.
+ * @param {string} [name] The name to extract initials from.
+ * @returns {string} The uppercase initials of the first and last name.
+ * @example
+ * // Returns 'JD'
+ * getUserInitials('John Doe');
+ * // Returns 'A'
+ * getUserInitials('Alice');
+ * // Returns ''
+ * getUserInitials();
+ */
+const getUserInitials = (name?: string) => {
+ if (!name) {
+ return '';
+ }
+ const [first, last] = name.split(' ');
+ return `${first?.[0] ?? ''}${last?.[0] ?? ''}`.toUpperCase();
+};
+
+export const UserIcon = ({
+ userView,
+ className,
+ children,
+ onClick,
+ showTooltip = true,
+}: PropsWithChildren) => {
+ const { user, lastActiveAt } = userView;
+ const isActive = dateTime(lastActiveAt).diff(dateTime(), 'minutes', true) >= -15;
+ const theme = useTheme2();
+ const styles = useMemo(() => getStyles(theme, isActive), [theme, isActive]);
+ const content = (
+
+ );
+
+ if (showTooltip) {
+ const tooltip = (
+