Compare commits

...

13 Commits

Author SHA1 Message Date
Nick Fitzpatrick
3a24b1d6f2 force pr-env 2025-02-19 13:15:58 -08:00
Nick Fitzpatrick
8d1417fae3 updating endpoints 2025-02-19 12:13:35 -08:00
Nick Fitzpatrick
3e856dd595 dashboard info sidebar 2025-02-19 11:43:22 -08:00
Nathan Voxland
4620e5e090 Changed activity API to not need /model 2025-02-19 11:42:46 -08:00
Nathan Voxland
0b9c281dd2 Added model type to /api/activity/model API 2025-02-19 10:56:50 -08:00
Nathan Voxland
e8d704426d Fixed add-picture-url 2025-02-19 10:44:50 -08:00
Nathan Voxland
80653aca1a Added /api/activity/model/ID/view-history 2025-02-19 10:23:20 -08:00
Nick Fitzpatrick
232d420d37 using picture_url 2025-02-19 09:19:29 -08:00
Nathan Voxland
edb9c2696e Fixed add-picture-url problem 2025-02-18 17:15:48 -08:00
Nick Fitzpatrick
1101d717a3 lunk avatar 2025-02-18 15:13:40 -08:00
Nathan Voxland
0abe5f9001 Moved changesets to v53 not 52 2025-02-18 15:10:49 -08:00
Nathan Voxland
b39a68d2f2 Added "show_picture" user setting that impacts picture_url result 2025-02-18 15:02:59 -08:00
Nathan Voxland
0c36d48a54 Added picture_url to user API with URL to use for an avatar 2025-02-18 14:40:44 -08:00
29 changed files with 297 additions and 77 deletions

View File

@@ -148,8 +148,8 @@ jobs:
e2e-tests-result:
needs:
- e2e-tests
- flaky-e2e-tests
- e2e-tests
- flaky-e2e-tests
runs-on: ubuntu-latest
timeout-minutes: 5
name: e2e-tests-result
@@ -175,8 +175,8 @@ jobs:
pr-env:
needs: [build]
if: |
!cancelled() &&
contains(github.event.pull_request.labels.*.name, 'PR-Env')
# if: |
# !cancelled() &&
# contains(github.event.pull_request.labels.*.name, 'PR-Env')
uses: ./.github/workflows/pr-env.yml
secrets: inherit

View File

@@ -20,6 +20,7 @@ export interface BaseUser {
date_joined: string;
last_login: string;
first_login: string;
picture_url?: string;
}
export interface User extends BaseUser {

View File

@@ -0,0 +1,12 @@
.HoverTarget {
&:hover {
.ShowOnHover {
opacity: 1;
}
}
}
.ShowOnHover {
opacity: 0;
transition: opacity 100ms ease;
}

View File

@@ -1,6 +1,5 @@
import styled from "@emotion/styled";
import UserAvatar from "metabase/components/UserAvatar";
import {
breakpointMinMedium,
breakpointMinSmall,
@@ -42,20 +41,3 @@ export const HeaderSubtitle = styled.h3`
text-align: center;
color: var(--mb-color-text-medium);
`;
export const HeaderAvatar = styled(UserAvatar)`
width: 3em;
height: 3em;
margin-bottom: ${space(1)};
${breakpointMinSmall} {
width: 4em;
height: 4em;
margin-bottom: ${space(2)};
}
${breakpointMinMedium} {
width: 5em;
height: 5em;
}
`;

View File

@@ -1,15 +1,17 @@
import type { Path } from "history";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { t } from "ttag";
import { UserAvatar } from "metabase/components/UserAvatar";
import Radio from "metabase/core/components/Radio";
import { getFullName } from "metabase/lib/user";
import { PLUGIN_IS_PASSWORD_USER } from "metabase/plugins";
import { Box, Button, Flex, Icon, Indicator, Modal } from "metabase/ui";
import type { User } from "metabase-types/api";
import S from "./AccountHeader.module.css";
import {
AccountHeaderRoot,
HeaderAvatar,
HeaderSection,
HeaderSubtitle,
HeaderTitle,
@@ -26,6 +28,7 @@ export const AccountHeader = ({
path,
onChangeLocation,
}: AccountHeaderProps) => {
const [modalOpen, setModalOpen] = useState(false);
const hasPasswordChange = useMemo(
() => PLUGIN_IS_PASSWORD_USER.every(predicate => predicate(user)),
[user],
@@ -48,7 +51,28 @@ export const AccountHeader = ({
return (
<AccountHeaderRoot data-testid="account-header">
<HeaderSection>
<HeaderAvatar user={user} />
{/* <HeaderAvatar user={user} /> */}
<Indicator
label={<Icon name="pencil" />}
size={30}
offset={12}
color="brand"
classNames={{
root: S.HoverTarget,
indicator: S.ShowOnHover,
}}
>
<UserAvatar
mb="1rem"
user={user}
name={userFullName ?? undefined}
color="brand"
variant="filled"
size="xl"
mayor
onClick={() => setModalOpen(true)}
/>
</Indicator>
{userFullName && <HeaderTitle>{userFullName}</HeaderTitle>}
<HeaderSubtitle>{user.email}</HeaderSubtitle>
</HeaderSection>
@@ -58,6 +82,45 @@ export const AccountHeader = ({
options={tabs}
onChange={onChangeLocation}
/>
<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
withCloseButton={false}
>
<Box>
<Box
bg="bg-medium"
pos="absolute"
w="100%"
h="125px"
top={0}
left={0}
/>
<Flex direction="column" align="center" pt="lg">
<Box
bg="bg-medium"
p="sm"
style={{ borderRadius: "100%" }}
mb="1rem"
>
<UserAvatar
user={user}
name={userFullName ?? undefined}
color="brand"
variant="filled"
size="7.5rem"
/>
</Box>
<Button variant="subtle" mb="1rem">
Upload new image
</Button>
<Flex gap="1rem">
<Button>Remove Image</Button>
<Button>Use Initials</Button>
</Flex>
</Flex>
</Box>
</Modal>
</AccountHeaderRoot>
);
};

View File

@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { Component } from "react";
import { t } from "ttag";
import UserAvatar from "metabase/components/UserAvatar";
import { UserAvatar } from "metabase/components/UserAvatar";
import CS from "metabase/css/core/index.css";
import RevisionDiff from "./RevisionDiff";

View File

@@ -4,7 +4,7 @@ import { useMemo, useRef, useState } from "react";
import { t } from "ttag";
import TippyPopover from "metabase/components/Popover/TippyPopover";
import UserAvatar from "metabase/components/UserAvatar";
import { UserAvatar } from "metabase/components/UserAvatar";
import CS from "metabase/css/core/index.css";
import Typeahead from "metabase/hoc/Typeahead";
import { color } from "metabase/lib/colors";

View File

@@ -12,7 +12,7 @@ import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapp
import ModalContent from "metabase/components/ModalContent";
import ModalWithTrigger from "metabase/components/ModalWithTrigger";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import UserAvatar from "metabase/components/UserAvatar";
import { UserAvatar } from "metabase/components/UserAvatar";
import Input from "metabase/core/components/Input";
import Link from "metabase/core/components/Link";
import AdminS from "metabase/css/admin.module.css";

View File

@@ -6,7 +6,7 @@ import { t } from "ttag";
import EntityMenu from "metabase/components/EntityMenu";
import LoadingSpinner from "metabase/components/LoadingSpinner";
import UserAvatar from "metabase/components/UserAvatar";
import { UserAvatar } from "metabase/components/UserAvatar";
import Tooltip from "metabase/core/components/Tooltip";
import CS from "metabase/css/core/index.css";
import { color } from "metabase/lib/colors";

View File

@@ -58,6 +58,9 @@ export const activityApi = Api.injectEndpoints({
]),
},
),
listPresence: builder.query({
query: ({ model, id }) => `/api/activity/${model}/${id}/view-history`,
}),
}),
});
@@ -65,4 +68,5 @@ export const {
useListRecentsQuery,
useListPopularItemsQuery,
useLogRecentItemMutation,
useListPresenceQuery,
} = activityApi;

View File

@@ -17,6 +17,7 @@ import {
TimelineEvent,
Timestamp,
} from "./Timeline.styled";
import { UserAvatar } from "metabase/components/UserAvatar";
interface TimelineProps {
events: RevisionOrModerationEvent[];
@@ -43,7 +44,8 @@ export function Timeline({
return (
<TimelineEvent key={revision?.id ?? `${title}-${timestamp}`}>
{isNotLastEvent && <Border />}
<EventIcon icon={icon} />
{/* <EventIcon icon={icon} /> */}
<UserAvatar user={revision?.user} size="md" />
<EventBody>
<EventHeader>
<span>{title}</span>

View File

@@ -7,6 +7,7 @@ export interface AvatarProps {
height?: string[];
width?: string[];
bg?: string;
src?: string;
}
export const Avatar = styled.div<AvatarProps>`
@@ -21,4 +22,6 @@ export const Avatar = styled.div<AvatarProps>`
line-height: 1;
background-color: ${({ bg = color("brand") }) => bg};
flex-shrink: 0;
${({ src }) =>
src ? `background-image: url(${src}); background-size: cover;` : ""}
`;

View File

@@ -1,21 +1,15 @@
import { isEmail } from "metabase/lib/email";
import type { AvatarProps } from "./UserAvatar.styled";
import { Avatar as StyledAvatar } from "./UserAvatar.styled";
import { Avatar, type AvatarProps, Box, Flex, Icon } from "metabase/ui";
import type { User } from "metabase-types/api";
interface UserAvatarProps extends AvatarProps {
user: User;
mayor?: boolean;
}
interface GroupProps {
user: Group;
}
interface User {
first_name: string | null;
last_name: string | null;
common_name: string;
email?: string;
mayor?: boolean;
}
interface Group {
@@ -23,15 +17,23 @@ interface Group {
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default function UserAvatar({
export function UserAvatar({
user,
mayor = false,
...props
}: UserAvatarProps | GroupProps) {
return <StyledAvatar {...props}>{userInitials(user) || "?"}</StyledAvatar>;
}
export function Avatar({ children, ...props }: { children: string }) {
return <StyledAvatar {...props}>{initial(children) ?? "?"}</StyledAvatar>;
return (
<Box pos="relative">
<Avatar {...props} src={user.picture_url}>
{userInitials(user)}
</Avatar>
{mayor && (
<Flex pos="absolute" top={-15} justify="center" left={0} right={0}>
<Icon name="crown" size={32} color="gold" />
</Flex>
)}
</Box>
);
}
function initial(name?: string | null) {

View File

@@ -1 +1 @@
export { default, Avatar } from "./UserAvatar";
export { UserAvatar } from "./UserAvatar";

View File

@@ -2,7 +2,7 @@ import cx from "classnames";
import { useState } from "react";
import { c, t } from "ttag";
import { skipToken, useGetUserQuery } from "metabase/api";
import { skipToken, useGetUserQuery, useListPresenceQuery } from "metabase/api";
import { SidesheetCardSection } from "metabase/common/components/Sidesheet";
import DateTime from "metabase/components/DateTime";
import Link from "metabase/core/components/Link";
@@ -14,6 +14,9 @@ import { Box, FixedSizeIcon, Flex, Text } from "metabase/ui";
import type { Dashboard } from "metabase-types/api";
import SidebarStyles from "./DashboardInfoSidebar.module.css";
import { UserAvatar } from "metabase/components/UserAvatar";
import { Avatar, Pill } from "@mantine/core";
import { getRelativeTime, getRelativeTimeAbbreviated } from "metabase/lib/time";
export const DashboardDetails = ({ dashboard }: { dashboard: Dashboard }) => {
const lastEditInfo = dashboard["last-edit-info"];
@@ -21,24 +24,39 @@ export const DashboardDetails = ({ dashboard }: { dashboard: Dashboard }) => {
// we don't hydrate creator user info on the dashboard object
const { data: creator } = useGetUserQuery(dashboard.creator_id ?? skipToken);
const { data: views } = useListPresenceQuery({
id: dashboard.id,
model: "dashboard",
});
console.log(views);
return (
<>
<SidesheetCardSection title={t`Creator and last editor`}>
<SidesheetCardSection title={t`Created by`}>
{creator && (
<Flex gap="sm" align="top">
<FixedSizeIcon name="ai" className={SidebarStyles.IconMargin} />
<Text>
{c(
"Describes when a dashboard was created. {0} is a date/time and {1} is a person's name",
).jt`${(
<DateTime unit="day" value={createdAt} key="date" />
)} by ${getUserName(creator)}`}
</Text>
<Flex gap="sm" align="center">
<UserAvatar user={creator} />
<Flex direction="column" justify="center">
<Flex gap="0.5rem">
<Text fw="bold">{`${creator.first_name} ${creator.last_name?.[0]}`}</Text>{" "}
{creator.is_superuser && (
<Pill
size="sm"
c="var(--mb-base-color-octopus-80)"
bg="var(--mb-base-color-octopus-20)"
>
Admin
</Pill>
)}
</Flex>
<Text>{creator.email}</Text>
</Flex>
<Text>{getRelativeTimeAbbreviated(createdAt)}</Text>
</Flex>
)}
{lastEditInfo && (
{/* {lastEditInfo && (
<Flex gap="sm" align="top">
<FixedSizeIcon name="pencil" className={SidebarStyles.IconMargin} />
<Text>
@@ -53,7 +71,7 @@ export const DashboardDetails = ({ dashboard }: { dashboard: Dashboard }) => {
)} by ${getUserName(lastEditInfo)}`}
</Text>
</Flex>
)}
)} */}
</SidesheetCardSection>
{dashboard.collection && (
<SidesheetCardSection
@@ -77,6 +95,15 @@ export const DashboardDetails = ({ dashboard }: { dashboard: Dashboard }) => {
</Flex>
</SidesheetCardSection>
)}
<SidesheetCardSection title={t`Knowledgable folks`}>
{views && (
<Avatar.Group>
{views.map((view, index) => (
<UserAvatar mayor={index === 0} user={view} size="sm" />
))}
</Avatar.Group>
)}
</SidesheetCardSection>
<SharingDisplay dashboard={dashboard} />
</>
);

View File

@@ -3,7 +3,7 @@ import { t } from "ttag";
import { useSetting } from "metabase/common/hooks";
import TokenField from "metabase/components/TokenField";
import UserAvatar from "metabase/components/UserAvatar";
import { UserAvatar } from "metabase/components/UserAvatar";
import CS from "metabase/css/core/index.css";
import { isEmail } from "metabase/lib/email";
import { recipientIsValid } from "metabase/lib/pulse";

View File

@@ -34,7 +34,9 @@ export const clearCurrentUser = createAction(
export const currentUser = createReducer<User | null>(null, builder => {
builder
.addCase(clearCurrentUser, () => null)
.addCase(refreshCurrentUser.fulfilled, (state, action) => action.payload)
.addCase(refreshCurrentUser.fulfilled, (state, action) => ({
...action.payload,
}))
.addCase(CLOSE_QB_NEWB_MODAL, state => {
if (state) {
state.is_qbnewb = false;

View File

@@ -0,0 +1,11 @@
import { Avatar } from "@mantine/core";
export const avatarOverrides = {
Avatar: Avatar.extend({
defaultProps: {
size: "lg",
color: "brand",
variant: "filled",
},
}),
};

View File

@@ -0,0 +1,2 @@
export { Avatar, type AvatarProps } from "@mantine/core";
export * from "./Avatar.config";

View File

@@ -1,4 +1,5 @@
export * from "./Accordion";
export * from "./Avatar";
export * from "./Badge";
export * from "./Card";
export * from "./Image";

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 20" fill="currentcolor" stroke-width="5">
<path d="M1.60409 6.6748L3.57529 16.3049C3.57529 16.3147 3.57849 16.3246 3.58169 16.3377L4.05209 18.6304C4.06489 18.6862 4.08409 18.7354 4.11289 18.7813C4.11609 18.7878 4.12249 18.7911 4.12569 18.7977C4.15449 18.837 4.18329 18.8731 4.22169 18.8994C4.23449 18.9092 4.25369 18.9158 4.26969 18.9256C4.30169 18.9453 4.33369 18.9617 4.37209 18.9715C4.38169 18.9748 4.38809 18.9814 4.39449 18.9814C7.17849 19.598 9.9817 19.9063 12.7913 19.9063C15.5977 19.9063 18.4073 19.598 21.2009 18.9814C21.2105 18.9781 21.2169 18.9748 21.2233 18.9715C21.2585 18.9617 21.2937 18.9453 21.3257 18.9256C21.3417 18.9158 21.3577 18.9092 21.3737 18.8994C21.4089 18.8731 21.4377 18.8403 21.4633 18.8042C21.4697 18.7944 21.4793 18.7911 21.4825 18.7813C21.5113 18.7354 21.5305 18.6862 21.5433 18.6304L21.5465 18.6206L22.0137 16.3344C22.0169 16.3246 22.0169 16.3147 22.0201 16.3049L23.9881 6.6748C24.5321 6.56656 25.0345 6.23856 25.3321 5.7072C25.5977 5.23816 25.6681 4.6904 25.5305 4.1656C25.3929 3.6408 25.0665 3.20128 24.6057 2.93232C24.1481 2.66008 23.6137 2.58792 23.1017 2.72896C22.5897 2.87 22.1609 3.20456 21.8953 3.67688C21.6297 4.14592 21.5593 4.69368 21.6969 5.21848C21.7065 5.25456 21.7257 5.28736 21.7353 5.32344L16.5577 9.20368L14.1161 3.5424C14.5225 3.16848 14.7817 2.63384 14.7817 2.0336C14.7817 0.91184 13.8921 0 12.7977 0C11.7033 0 10.8137 0.91184 10.8137 2.0336C10.8137 2.63384 11.0729 3.16848 11.4793 3.5424L9.03769 9.20368L3.85369 5.32344C4.02649 4.78224 3.98169 4.182 3.69689 3.6736C3.43129 3.20456 3.00249 2.87 2.49049 2.72896C1.97849 2.58792 1.44409 2.66336 0.989694 2.9356C0.0456935 3.48992 -0.280707 4.73632 0.263293 5.71376C0.554493 6.22544 1.05049 6.56328 1.60409 6.6748ZM20.7305 18.1417C15.4569 19.2667 10.1225 19.2667 4.86489 18.1417L4.59289 16.8166C7.31289 17.3873 10.0489 17.6858 12.7913 17.6858C15.5305 17.6858 18.2729 17.3873 21.0025 16.8166L20.7305 18.1417ZM22.6729 4.1328C22.8201 3.87368 23.0537 3.69 23.3353 3.61128C23.4281 3.58504 23.5241 3.57192 23.6201 3.57192C23.8089 3.57192 23.9945 3.62112 24.1641 3.7228C24.4169 3.8704 24.5961 4.11312 24.6729 4.40176C24.7497 4.6904 24.7081 4.99216 24.5641 5.248C24.4169 5.50712 24.1833 5.6908 23.9017 5.76952C23.6201 5.84496 23.3257 5.8056 23.0761 5.658C22.8233 5.50712 22.6441 5.26768 22.5673 4.97904C22.4873 4.69368 22.5257 4.39192 22.6729 4.1328ZM12.7977 0.91512C13.3993 0.91512 13.8889 1.41696 13.8889 2.0336C13.8889 2.65024 13.3993 3.15208 12.7977 3.15208C12.1961 3.15208 11.7065 2.65024 11.7065 2.0336C11.7097 1.41696 12.1993 0.91512 12.7977 0.91512ZM8.96089 10.2795C8.97049 10.2861 8.98009 10.2828 8.98969 10.2894C9.00889 10.3025 9.02169 10.3189 9.04409 10.3287C9.10169 10.355 9.16249 10.3681 9.22329 10.3681C9.30009 10.3681 9.37689 10.3418 9.44409 10.3025C9.46649 10.2894 9.48249 10.2697 9.50169 10.2533C9.53049 10.2303 9.56249 10.2139 9.58489 10.1811C9.59769 10.1614 9.60089 10.1418 9.61049 10.1221C9.61689 10.1122 9.62649 10.109 9.63289 10.0958L12.2665 3.9852C12.4361 4.0344 12.6121 4.0672 12.7977 4.0672C12.9833 4.0672 13.1593 4.03112 13.3289 3.9852L15.9625 10.0958C15.9657 10.1057 15.9785 10.1122 15.9849 10.1221C15.9945 10.1418 15.9977 10.1647 16.0105 10.1811C16.0329 10.2139 16.0649 10.2303 16.0937 10.2533C16.1129 10.2697 16.1289 10.2894 16.1513 10.3025C16.2185 10.3418 16.2953 10.3681 16.3721 10.3681C16.4329 10.3681 16.4937 10.355 16.5513 10.3287C16.5737 10.3189 16.5865 10.3025 16.6057 10.2894C16.6153 10.2828 16.6249 10.2861 16.6345 10.2795L22.2025 6.11064C22.3273 6.24184 22.4649 6.35992 22.6217 6.45504C22.7689 6.5436 22.9225 6.60264 23.0793 6.64856L21.1977 15.8457C15.6105 17.0691 9.95929 17.0691 4.39129 15.8457L2.50969 6.65184C2.66969 6.60592 2.82329 6.54032 2.97369 6.45176C3.13049 6.35664 3.27129 6.24184 3.39609 6.11392L8.96089 10.2795ZM1.43769 3.72608C1.60729 3.6244 1.79289 3.5752 1.98169 3.5752C2.07449 3.5752 2.17049 3.58832 2.26329 3.61128C2.54489 3.68672 2.78169 3.87368 2.92569 4.12952C3.12089 4.4772 3.12089 4.90688 2.92569 5.25128C2.82969 5.42184 2.69209 5.55632 2.52569 5.658C2.00409 5.96632 1.32569 5.76296 1.03769 5.25456C0.740093 4.71664 0.919294 4.03112 1.43769 3.72608Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -101,6 +101,8 @@ import contract_component from "./contract.svg?component";
import contract_source from "./contract.svg?source";
import copy_component from "./copy.svg?component";
import copy_source from "./copy.svg?source";
import crown_component from "./crown.svg?component";
import crown_source from "./crown.svg?source";
import curve_component from "./curve.svg?component";
import curve_source from "./curve.svg?source";
import curved_component from "./curved.svg?component";
@@ -603,6 +605,10 @@ export const Icons = {
component: copy_component,
source: copy_source,
},
crown: {
component: crown_component,
source: crown_source,
},
curved: {
component: curved_component,
source: curved_source,

View File

@@ -9,6 +9,7 @@ import {
alertOverrides,
anchorOverrides,
autocompleteOverrides,
avatarOverrides,
badgeOverrides,
buttonOverrides,
calendarOverrides,
@@ -120,6 +121,7 @@ export const getThemeOverrides = (): MantineThemeOverride => ({
...actionIconOverrides,
...alertOverrides,
...anchorOverrides,
...avatarOverrides,
...autocompleteOverrides,
...badgeOverrides,
...buttonOverrides,

View File

@@ -10509,6 +10509,38 @@ databaseChangeLog:
type: ${timestamp_type}
remarks: The timestamp at which a user was deactivated
- changeSet:
id: v53.2025-02-18T21:32:11
author: nvoxland
comment: Add automatic picture URL
preConditions:
changes:
- addColumn:
tableName: core_user
columns:
- column:
name: auto_picture_url
type: varchar(254)
remarks: URL to a picture of the user that is automatically managed
- changeSet:
id: v53.2025-02-18T21:32:12
author: nvoxland
comment: Add "show_picture" setting for avatar pictures
preConditions:
changes:
- addColumn:
tableName: core_user
columns:
- column:
name: show_picture
type: boolean
valueBoolean: true
defaultValueBoolean: true
remarks: Should the user's picture be shown if available
constraints:
nullable: false
- changeSet:
id: v54.2025-01-30T16:10:55
author: bshepherdson

View File

@@ -7,6 +7,7 @@
[metabase.api.macros :as api.macros]
[metabase.db.query :as mdb.query]
[metabase.models.interface :as mi]
[metabase.models.user :as user]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
@@ -240,3 +241,34 @@
archived, deleted, etc it can usually still get 5. "
[]
{:popular_items (get-popular-items-model-and-id)})
(defn- view-history [model_id model_type]
(map #(-> %
user/add-picture-url
user/add-common-name)
(t2/select :model/ViewLog
{:select [:user_id :first_name :last_name :email :auto_picture_url :show_picture [:%max.timestamp :latest_timestamp] [:%count.* :view_count]]
:from [[:view_log :l]]
:join [[:core_user :u] [:= :u.id :l.user_id]]
:where [:and [:= :model_id model_id] [:= :model model_type]]
:group-by [:user_id :first_name :last_name :email :auto_picture_url :show_picture]
:order-by [[:latest_timestamp :desc]]}))
)
(api.macros/defendpoint :get "/card/:id/view-history"
"Return who has viewed this card."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(view-history id "card"))
(api.macros/defendpoint :get "/dashboard/:id/view-history"
"Return who has viewed this dashboard."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(view-history id "dashboard"))
(api.macros/defendpoint :get "/collection/:id/view-history"
"Return who has viewed this collection."
[{:keys [id]} :- [:map
[:id ms/PositiveInt]]]
(view-history id "collection"))

View File

@@ -189,13 +189,24 @@
common-name)
(assoc :common_name common-name))))
(defn add-picture-url
[{:keys [auto_picture_url show_picture], :as user}]
(if (boolean show_picture)
(if (boolean auto_picture_url)
(assoc user :picture_url auto_picture_url)
(assoc user :picture_url nil))
(assoc user :picture_url nil)))
(t2/define-after-select :model/User
[user]
(add-common-name user))
(-> user
add-common-name
add-picture-url
))
(def ^:private default-user-columns
"Sequence of columns that are normally returned when fetching a User from the DB."
[:id :email :date_joined :first_name :last_name :last_login :is_superuser :is_qbnewb])
[:id :email :date_joined :first_name :last_name :last_login :is_superuser :is_qbnewb :auto_picture_url :show_picture])
(def admin-or-self-visible-columns
"Sequence of columns that we can/should return for admins fetching a list of all Users, or for the current user
@@ -321,7 +332,9 @@
[:login_attributes {:optional true} [:maybe LoginAttributes]]
[:sso_source {:optional true} [:maybe ms/NonBlankString]]
[:locale {:optional true} [:maybe ms/KeywordOrString]]
[:type {:optional true} [:maybe ms/KeywordOrString]]])
[:type {:optional true} [:maybe ms/KeywordOrString]]
[:auto_picture_url {:optional true} [:maybe ms/NonBlankString]]
[:show_picture {:optional true} [:maybe :boolean]]])
(def ^:private Invitor
"Map with info about the admin creating the user, used in the new user notification code"

View File

@@ -161,7 +161,7 @@
(merge (revision-description-info model prev-revision revision))
;; add revision user details
(t2/hydrate :user)
(update :user select-keys [:id :first_name :last_name :common_name])
(update :user select-keys [:id :first_name :last_name :common_name :picture_url])
;; Filter out irrelevant info
(dissoc :model :model_id :user_id :object)))

View File

@@ -63,26 +63,29 @@
(defn- maybe-update-google-user!
"Update google user if the first or list name changed."
[user first-name last-name]
[user first-name last-name google_picture_url]
(when (or (not= first-name (:first_name user))
(not= last-name (:last_name user)))
(not= last-name (:last_name user))
(not= google_picture_url (:auto_picture_url user)))
(t2/update! :model/User (:id user) {:first_name first-name
:last_name last-name}))
(assoc user :first_name first-name :last_name last-name))
:last_name last-name
:auto_picture_url google_picture_url}))
(assoc user :first_name first-name :last_name last-name :auto_picture_url google_picture_url))
(mu/defn- google-auth-fetch-or-create-user! :- (ms/InstanceOf :model/User)
[first-name last-name email]
(let [existing-user (t2/select-one [:model/User :id :email :last_login :first_name :last_name] :%lower.email (u/lower-case-en email))]
[first-name last-name email google_picture_url]
(let [existing-user (t2/select-one [:model/User :id :email :last_login :first_name :last_name :auto_picture_url] :%lower.email (u/lower-case-en email))]
(if existing-user
(maybe-update-google-user! existing-user first-name last-name)
(maybe-update-google-user! existing-user first-name last-name google_picture_url)
(google-auth-create-new-user! {:first_name first-name
:last_name last-name
:email email}))))
:email email
:auto_picture_url google_picture_url}))))
(defn do-google-auth
"Call to Google to perform an authentication"
[{{:keys [token]} :body, :as _request}]
(let [token-info-response (http/post (format google-auth-token-info-url token))
{:keys [given_name family_name email]} (google-auth-token-info token-info-response)]
{:keys [given_name family_name email picture]} (google-auth-token-info token-info-response)]
(log/infof "Successfully authenticated Google Sign-In token for: %s %s" given_name family_name)
(api/check-500 (google-auth-fetch-or-create-user! given_name family_name email))))
(api/check-500 (google-auth-fetch-or-create-user! given_name family_name email picture))))

View File

@@ -601,3 +601,20 @@
(t2/update! :model/User user-id {:is_active true})
(let [deactivated-at (t2/select-one-fn :deactivated_at :model/User user-id)]
(is (nil? deactivated-at))))))
(deftest add-picture-url
(testing "Sets picture if available"
(is (= {:auto_picture_url "http://example.com/avatar"
:show_picture true :id 4 :picture_url "http://example.com/avatar"}
(user/add-picture-url {:auto_picture_url "http://example.com/avatar" :show_picture true :id 4}))))
(testing "Null picture if not available"
(is (= {:auto_picture_url nil
:show_picture true :id 4 :picture_url nil}
(user/add-picture-url {:auto_picture_url nil :show_picture true :id 4}))))
(testing "Null picture if not show_picture"
(is (= {:auto_picture_url "http://example.com/avatar"
:show_picture false :id 4 :picture_url nil}
(user/add-picture-url {:auto_picture_url "http://example.com/avatar" :show_picture false :id 4}))))
(is (= {:auto_picture_url nil
:show_picture false :id 4 :picture_url nil}
(user/add-picture-url {:auto_picture_url nil :show_picture false :id 4}))))