Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add user group db schema support #1144

Merged
merged 8 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querybook",
"version": "3.17.1",
"version": "3.18.0",
"description": "A Big Data Webapp",
"private": true,
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""add user group support

Revision ID: 1b8aba201c94
Revises: 27ed76f75106
Create Date: 2023-02-03 00:31:09.209132

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = "1b8aba201c94"
down_revision = "27ed76f75106"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"user_group_member",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("gid", sa.Integer(), nullable=False),
sa.Column("uid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["gid"], ["user.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["uid"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.add_column("user", sa.Column("is_group", sa.Boolean(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "is_group")
op.drop_table("user_group_member")
# ### end Alembic commands ###
10 changes: 10 additions & 0 deletions querybook/server/datasources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ def get_user_info(uid):
return user


@register("/user/<int:uid>/group_members/", methods=["GET"])
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved
def get_user_group_members(uid):
group = logic.get_user_by_id(uid)
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved

if group is None:
abort(RESOURCE_NOT_FOUND_STATUS_CODE)

return [g.to_dict() for g in group.group_members]
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved


@register("/user/name/<username>/", methods=["GET"])
def get_user_info_by_username(username):
user = logic.get_user_by_name(username)
Expand Down
5 changes: 5 additions & 0 deletions querybook/server/logic/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def get_user_by_name(username, case_sensitive=True, session=None):
return None


@with_session
def get_user_group_members(id, session=None):
return User.get(id=id, session=session).group_members


@with_session
def create_user(
username,
Expand Down
20 changes: 20 additions & 0 deletions querybook/server/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@ class User(CRUDMixin, Base):
email = sql.Column(sql.String(length=name_length))
profile_img = sql.Column(sql.String(length=url_length))
deleted = sql.Column(sql.Boolean, default=False)
is_group = sql.Column(sql.Boolean, default=False)

properties = sql.Column(sql.JSON, default={})

settings = relationship("UserSetting", cascade="all, delete", passive_deletes=True)
roles = relationship("UserRole", cascade="all, delete", passive_deletes=True)
group_members = relationship(
"User",
secondary="user_group_member",
primaryjoin="User.id == UserGroupMember.gid",
secondaryjoin="User.id == UserGroupMember.uid",
backref="user_groups",
)

@hybrid_property
def password(self):
Expand Down Expand Up @@ -66,8 +74,12 @@ def to_dict(self, with_roles=False):
"profile_img": self.profile_img,
"email": self.email,
"deleted": self.deleted,
"is_group": self.is_group,
}

if self.is_group:
user_dict["properties"] = self.properties
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved

if with_roles:
user_dict["roles"] = [role.role.value for role in self.roles]

Expand Down Expand Up @@ -107,3 +119,11 @@ def to_dict(self):
"role": self.role.value,
"created_at": self.created_at,
}


class UserGroupMember(Base):
__tablename__ = "user_group_member"

id = sql.Column(sql.Integer, primary_key=True)
gid = sql.Column(sql.Integer, sql.ForeignKey("user.id", ondelete="CASCADE"))
uid = sql.Column(sql.Integer, sql.ForeignKey("user.id", ondelete="CASCADE"))
czgu marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ export const DataTableHeader: React.FunctionComponent<IDataTableHeader> = ({
);

const ownershipDOM = (tableOwnerships || []).map((ownership) => (
<UserBadge key={ownership.uid} uid={ownership.uid} mini cardStyle />
<UserBadge
key={ownership.uid}
uid={ownership.uid}
mini
cardStyle
groupPopover
/>
));

// Ownership cannot be removed if owner in db
Expand Down
59 changes: 41 additions & 18 deletions querybook/webapp/components/UserBadge/UserBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import clsx from 'clsx';
import React, { useMemo } from 'react';

import { UserGroupCard } from 'components/UserGroupCard/UserGroupCard';
import { DELETED_USER_MSG } from 'const/user';
import { useUser } from 'hooks/redux/useUser';
import { Popover } from 'ui/Popover/Popover';
import { PopoverHoverWrapper } from 'ui/Popover/PopoverHoverWrapper';
import { AccentText } from 'ui/StyledText/StyledText';

import { ICommonUserLoaderProps } from './types';
Expand All @@ -15,6 +18,7 @@ type IProps = {
isOnline?: boolean;
mini?: boolean;
cardStyle?: boolean;
groupPopover?: boolean;
} & ICommonUserLoaderProps;

export const UserBadge: React.FunctionComponent<IProps> = ({
Expand All @@ -23,6 +27,7 @@ export const UserBadge: React.FunctionComponent<IProps> = ({
isOnline,
mini,
cardStyle,
groupPopover,
}) => {
const { loading, userInfo } = useUser({ uid, name });

Expand All @@ -46,24 +51,20 @@ export const UserBadge: React.FunctionComponent<IProps> = ({

const deletedText = userInfo?.deleted ? DELETED_USER_MSG : '';

if (mini) {
return (
<span
className={clsx({
UserBadge: true,
mini: true,
'card-style': cardStyle,
})}
>
<figure>{avatarDOM}</figure>
<AccentText className="username" weight="bold">
{userInfo?.fullname ?? userName} {deletedText}
</AccentText>
</span>
);
}

return (
const badgeDOM = mini ? (
<span
className={clsx({
UserBadge: true,
mini: true,
'card-style': cardStyle,
})}
>
<figure>{avatarDOM}</figure>
<AccentText className="username" weight="bold">
{userInfo?.fullname ?? userName} {deletedText}
</AccentText>
</span>
) : (
<div
className={clsx({
UserBadge: true,
Expand All @@ -89,4 +90,26 @@ export const UserBadge: React.FunctionComponent<IProps> = ({
</div>
</div>
);

return groupPopover && userInfo?.is_group ? (
czgu marked this conversation as resolved.
Show resolved Hide resolved
<PopoverHoverWrapper>
{(showPopover, anchorElement) => (
<>
{badgeDOM}

{showPopover && (
<Popover
onHide={() => null}
anchor={anchorElement}
layout={['right', 'top']}
>
<UserGroupCard userGroup={userInfo} />
</Popover>
)}
</>
)}
</PopoverHoverWrapper>
) : (
badgeDOM
);
};
9 changes: 9 additions & 0 deletions querybook/webapp/components/UserGroupCard/UserGroupCard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.UserGroupCard {
max-width: 400px;

.members-container {
display: flex;
align-items: center;
flex-wrap: wrap;
}
}
60 changes: 60 additions & 0 deletions querybook/webapp/components/UserGroupCard/UserGroupCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useEffect, useState } from 'react';

import { UserBadge } from 'components/UserBadge/UserBadge';
import { IUserInfo } from 'const/user';
import { UserResource } from 'resource/user';
import { AccentText } from 'ui/StyledText/StyledText';

import './UserGroupCard.scss';

interface IProps {
userGroup: IUserInfo;
}

export const UserGroupCard = ({ userGroup }: IProps) => {
const [members, setMembers] = useState([]);

useEffect(() => {
UserResource.getUserGroupMembers(userGroup.id).then(({ data }) => {
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved
setMembers(data);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className="UserGroupCard">
<div>
<AccentText color="dark" weight="bold">
{userGroup.fullname}
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved
</AccentText>
<AccentText color="light">{userGroup.username}</AccentText>
</div>
<div className="mt8">
<AccentText color="dark" weight="bold">
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved
Email
</AccentText>
<AccentText color="light">{userGroup.email}</AccentText>
</div>
<div className="mt8">
<AccentText color="dark" weight="bold">
Description
</AccentText>
<AccentText color="light">
{userGroup.properties?.description}
czgu marked this conversation as resolved.
Show resolved Hide resolved
</AccentText>
</div>
<div className="mt8">
<AccentText color="dark" weight="bold">
Group members
</AccentText>
<div className="members-container">
{members.map((m) => (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put a limit on how many member to show

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we provide "show more" to allow them to view all the members?

<div key={m.id} className="ml8">
<UserBadge uid={m.id} mini />
</div>
))}
</div>
</div>
</div>
);
};
5 changes: 5 additions & 0 deletions querybook/webapp/const/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ export interface IUserInfo {
fullname: string;
profile_img: string;
deleted: boolean;
email: string;
is_group: boolean;

roles?: number[];
properties?: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties is no longer optional

description?: string;
};
}

export interface IMyUserInfo {
Expand Down
2 changes: 2 additions & 0 deletions querybook/webapp/resource/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const UserResource = {
[visibleEnvironments: IEnvironment[], userEnvironmentIds: number[]]
>('/user/environment/'),
getNotifiers: () => ds.fetch<INotifier[]>('/user/notifiers/'),
getUserGroupMembers: (id: number) =>
ds.fetch<IUserInfo[]>(`/user/${id}/group_members/`),
};

export const UserSettingResource = {
Expand Down