Skip to content

Commit

Permalink
feat: add user group db schema support
Browse files Browse the repository at this point in the history
  • Loading branch information
jczhong84 committed Feb 4, 2023
1 parent bae5c53 commit 8e89c07
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 20 deletions.
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"])
def get_user_group_members(uid):
group = logic.get_user_by_id(uid)

if group is None:
abort(RESOURCE_NOT_FOUND_STATUS_CODE)

return [g.to_dict() for g in group.group_members]


@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

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"))
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 ? (
<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 }) => {
setMembers(data);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className="UserGroupCard">
<div>
<AccentText color="dark" weight="bold">
{userGroup.fullname}
</AccentText>
<AccentText color="light">{userGroup.username}</AccentText>
</div>
<div className="mt8">
<AccentText color="dark" weight="bold">
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}
</AccentText>
</div>
<div className="mt8">
<AccentText color="dark" weight="bold">
Group members
</AccentText>
<div className="members-container">
{members.map((m) => (
<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?: {
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

0 comments on commit 8e89c07

Please sign in to comment.