mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-04 22:53:27 +00:00
adding teams page (#91)
* adding teams page * adding API and Docs link to the appbar * minor authhook changes * minor changes * minor changes * making separate avatar component * teams can add users now * adding placeholder for no data * adding search in adduser modal * hiding delete action for user card * addressing comment
This commit is contained in:
parent
a2f244f8ea
commit
81c9966610
@ -0,0 +1,33 @@
|
|||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { Team } from 'Models';
|
||||||
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
|
import APIClient from './index';
|
||||||
|
|
||||||
|
export const getTeams: Function = (
|
||||||
|
arrQueryFields?: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const url = getURLWithQueryFields('/teams', arrQueryFields);
|
||||||
|
|
||||||
|
return APIClient.get(`${url}&limit=1000000`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeamByName: Function = (
|
||||||
|
name: string,
|
||||||
|
arrQueryFields?: string
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const url = getURLWithQueryFields(`/teams/name/${name}`, arrQueryFields);
|
||||||
|
|
||||||
|
return APIClient.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTeam: Function = (data: Team) => {
|
||||||
|
return APIClient.post('/teams', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const patchTeamDetail: Function = (id: string, data: Team) => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json-patch+json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.patch(`/teams/${id}`, data, configOptions);
|
||||||
|
};
|
@ -15,15 +15,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Team } from 'Models';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { TagsCategory } from '../../../pages/tags/tagsTypes';
|
import { TagsCategory } from '../../../pages/tags/tagsTypes';
|
||||||
import { Button } from '../../buttons/Button/Button';
|
import { Button } from '../../buttons/Button/Button';
|
||||||
|
type FormData = TagsCategory | Team;
|
||||||
type FormModalProp = {
|
type FormModalProp = {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSave: (data: TagsCategory) => void;
|
onSave: (data: TagsCategory) => void;
|
||||||
form: React.ElementType;
|
form: React.ElementType;
|
||||||
header: string;
|
header: string;
|
||||||
initialData: TagsCategory;
|
initialData: FormData;
|
||||||
};
|
};
|
||||||
type FormRef = {
|
type FormRef = {
|
||||||
fetchMarkDownData: () => string;
|
fetchMarkDownData: () => string;
|
||||||
@ -36,7 +38,7 @@ const FormModal = ({
|
|||||||
initialData,
|
initialData,
|
||||||
}: FormModalProp) => {
|
}: FormModalProp) => {
|
||||||
const formRef = useRef<FormRef>();
|
const formRef = useRef<FormRef>();
|
||||||
const [data, setData] = useState<TagsCategory>(initialData);
|
const [data, setData] = useState<FormData>(initialData);
|
||||||
|
|
||||||
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -131,7 +131,27 @@ const Appbar: React.FC = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NavLink
|
||||||
|
className="tw-nav focus:tw-no-underline"
|
||||||
|
data-testid="appbar-item"
|
||||||
|
style={navStyle(location.pathname.startsWith('/documents'))}
|
||||||
|
target="_blank"
|
||||||
|
to={{
|
||||||
|
pathname: 'https://docs.open-metadata.org/',
|
||||||
|
}}>
|
||||||
|
{/* <i className="fas fa-file-alt tw-pr-2" /> */}
|
||||||
|
<span>Docs</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
className="tw-nav focus:tw-no-underline"
|
||||||
|
data-testid="appbar-item"
|
||||||
|
style={navStyle(location.pathname.startsWith('/docs'))}
|
||||||
|
to={{
|
||||||
|
pathname: '/docs',
|
||||||
|
}}>
|
||||||
|
{/* <i className="fas fa-sitemap tw-pr-2" /> */}
|
||||||
|
<span>API</span>
|
||||||
|
</NavLink>
|
||||||
<div data-testid="dropdown-profile">
|
<div data-testid="dropdown-profile">
|
||||||
<DropDown
|
<DropDown
|
||||||
dropDownList={[
|
dropDownList={[
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Avatar = ({ name }: { name: string }) => {
|
||||||
|
const getBgColorByCode = (code: number) => {
|
||||||
|
if (code >= 65 && code <= 71) {
|
||||||
|
return '#B02AAC40';
|
||||||
|
}
|
||||||
|
if (code >= 72 && code <= 78) {
|
||||||
|
return '#7147E840';
|
||||||
|
}
|
||||||
|
if (code >= 79 && code <= 85) {
|
||||||
|
return '#FFC34E40';
|
||||||
|
} else {
|
||||||
|
return '#1890FF40';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tw-flex tw-justify-center tw-items-center tw-align-middle"
|
||||||
|
style={{
|
||||||
|
height: '36px',
|
||||||
|
width: '36px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: getBgColorByCode(name.charCodeAt(0)),
|
||||||
|
color: 'black',
|
||||||
|
}}>
|
||||||
|
<p>{name[0]}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Avatar;
|
@ -131,7 +131,9 @@ const RichTextEditor = forwardRef<editorRef, EditorProp>(
|
|||||||
onEditorStateChange={onEditorStateChange}
|
onEditorStateChange={onEditorStateChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="tw-pt-2">Using headings in markdown is not allowed</p>
|
<p className="tw-pt-2 tw-float-right tw-text-grey-muted">
|
||||||
|
Using headings in markdown is not allowed
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ export const navLinkDevelop = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const navLinkSettings = [
|
export const navLinkSettings = [
|
||||||
// { name: 'Teams', to: '/teams', disabled: false },
|
{ name: 'Teams', to: '/teams', disabled: false },
|
||||||
{ name: 'Tags', to: '/tags', disabled: false },
|
{ name: 'Tags', to: '/tags', disabled: false },
|
||||||
// { name: 'Store', to: '/store', disabled: false },
|
// { name: 'Store', to: '/store', disabled: false },
|
||||||
{ name: 'Services', to: '/services', disabled: false },
|
{ name: 'Services', to: '/services', disabled: false },
|
||||||
|
@ -36,5 +36,7 @@ export const useAuth = (pathname = '') => {
|
|||||||
isEmpty(userDetails) &&
|
isEmpty(userDetails) &&
|
||||||
isEmpty(newUser),
|
isEmpty(newUser),
|
||||||
isAuthenticatedRoute: isAuthenticatedRoute,
|
isAuthenticatedRoute: isAuthenticatedRoute,
|
||||||
|
isAuthDisabled: authDisabled,
|
||||||
|
isAdminUser: userDetails?.isAdmin,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -168,8 +168,8 @@ declare module 'Models' {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UserTeam = {
|
export type UserTeam = {
|
||||||
description?: string;
|
description: string;
|
||||||
href?: string;
|
href: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@ -178,11 +178,13 @@ declare module 'Models' {
|
|||||||
export type User = {
|
export type User = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
isBot: boolean;
|
isBot: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name: string;
|
||||||
profile: UserProfile;
|
profile: UserProfile;
|
||||||
teams: Array<UserTeam>;
|
teams: Array<UserTeam>;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
href: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormatedTableData = {
|
export type FormatedTableData = {
|
||||||
@ -281,7 +283,15 @@ declare module 'Models' {
|
|||||||
aggregations: Record<string, Sterm>;
|
aggregations: Record<string, Sterm>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
export type Team = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
href: string;
|
||||||
|
users: Array<UserTeam>;
|
||||||
|
owns: Array<UserTeam>;
|
||||||
|
};
|
||||||
export type ServiceCollection = {
|
export type ServiceCollection = {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -161,7 +161,7 @@ const TagsPage = () => {
|
|||||||
const fetchLeftPanel = () => {
|
const fetchLeftPanel = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tw-flex tw-justify-between tw-items-baseline tw-mb-3">
|
<div className="tw-flex tw-justify-between tw-items-baseline tw-mb-3 tw-border-b">
|
||||||
<h6 className="tw-heading">Tag Categories</h6>
|
<h6 className="tw-heading">Tag Categories</h6>
|
||||||
<Button
|
<Button
|
||||||
className="tw-h-7 tw-px-2"
|
className="tw-h-7 tw-px-2"
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
import { UserTeam } from 'Models';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from '../../components/buttons/Button/Button';
|
||||||
|
import Searchbar from '../../components/common/searchbar/Searchbar';
|
||||||
|
import UserCard from './UserCard';
|
||||||
|
type Props = {
|
||||||
|
header: string;
|
||||||
|
list: Array<UserTeam>;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSave: (data: Array<UserTeam>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddUsersModal = ({ header, list, onCancel, onSave }: Props) => {
|
||||||
|
const [selectedUsers, setSelectedusers] = useState<Array<string>>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
|
const selectionHandler = (id: string) => {
|
||||||
|
setSelectedusers((prevState) => {
|
||||||
|
if (prevState.includes(id)) {
|
||||||
|
const userArr = [...prevState];
|
||||||
|
const index = userArr.indexOf(id);
|
||||||
|
userArr.splice(index, 1);
|
||||||
|
|
||||||
|
return userArr;
|
||||||
|
} else {
|
||||||
|
return [...prevState, id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getUserCards = () => {
|
||||||
|
return list
|
||||||
|
.filter((user) => {
|
||||||
|
return (
|
||||||
|
user.description.includes(searchText) ||
|
||||||
|
user.name.includes(searchText)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((user, index) => {
|
||||||
|
const User = {
|
||||||
|
description: user.description,
|
||||||
|
name: user.name,
|
||||||
|
id: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserCard
|
||||||
|
isActionVisible
|
||||||
|
isCheckBoxes
|
||||||
|
isIconVisible
|
||||||
|
item={User}
|
||||||
|
key={index}
|
||||||
|
onSelect={selectionHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const users = list.filter((user) => {
|
||||||
|
return selectedUsers.includes(user.id);
|
||||||
|
});
|
||||||
|
onSave(users);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchAction = (searchValue: string) => {
|
||||||
|
setSearchText(searchValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog className="tw-modal ">
|
||||||
|
<div className="tw-modal-backdrop" />
|
||||||
|
<div className="tw-modal-container tw-max-h-90vh tw-max-w-3xl">
|
||||||
|
<div className="tw-modal-header">
|
||||||
|
<p className="tw-modal-title">{header}</p>
|
||||||
|
</div>
|
||||||
|
<div className="tw-modal-body">
|
||||||
|
<Searchbar
|
||||||
|
placeholder="Search for user..."
|
||||||
|
searchValue={searchText}
|
||||||
|
typingInterval={1500}
|
||||||
|
onSearch={handleSearchAction}
|
||||||
|
/>
|
||||||
|
<div className="tw-grid tw-grid-cols-3 tw-gap-4">
|
||||||
|
{getUserCards()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tw-modal-footer tw-justify-end">
|
||||||
|
<Button
|
||||||
|
className="tw-mr-2"
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
onClick={onCancel}>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUsersModal;
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Team } from 'Models';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import MarkdownWithPreview from '../../components/common/editor/MarkdownWithPreview';
|
||||||
|
|
||||||
|
type FormProp = {
|
||||||
|
saveData: (value: {}) => void;
|
||||||
|
initialData: Team;
|
||||||
|
};
|
||||||
|
type EditorContentRef = {
|
||||||
|
getEditorContent: () => string;
|
||||||
|
};
|
||||||
|
const Form: React.FC<FormProp> = forwardRef(
|
||||||
|
({ saveData, initialData }: FormProp, ref): JSX.Element => {
|
||||||
|
const [data, setData] = useState<Team>({
|
||||||
|
name: initialData.name,
|
||||||
|
description: initialData.description,
|
||||||
|
displayName: initialData.displayName,
|
||||||
|
id: initialData.id || '',
|
||||||
|
href: initialData.href || '',
|
||||||
|
owns: initialData.owns || [],
|
||||||
|
users: initialData.users || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const markdownRef = useRef<EditorContentRef>();
|
||||||
|
|
||||||
|
const onChangeHadler = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
e.persist();
|
||||||
|
setData((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
fetchMarkDownData() {
|
||||||
|
return markdownRef.current?.getEditorContent();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveData({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tw-w-full tw-flex ">
|
||||||
|
<div className="tw-flex tw-w-full">
|
||||||
|
<div className="tw-w-full">
|
||||||
|
<div className="tw-mb-4">
|
||||||
|
<label className="tw-form-label required-field">Name</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
name="name"
|
||||||
|
placeholder="Name"
|
||||||
|
type="text"
|
||||||
|
value={data.name}
|
||||||
|
onChange={onChangeHadler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tw-mb-4">
|
||||||
|
<label className="tw-form-label required-field">
|
||||||
|
Display name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
name="displayName"
|
||||||
|
placeholder="Display name"
|
||||||
|
type="text"
|
||||||
|
value={data.displayName}
|
||||||
|
onChange={onChangeHadler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="tw-form-label required-field">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<MarkdownWithPreview ref={markdownRef} value={data.description} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Form.displayName = 'TeamsForm';
|
||||||
|
|
||||||
|
export default Form;
|
@ -0,0 +1,72 @@
|
|||||||
|
import { capitalize } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Avatar from '../../components/common/avatar/Avatar';
|
||||||
|
import { getPartialNameFromFQN } from '../../utils/CommonUtils';
|
||||||
|
import SVGIcons from '../../utils/SvgUtils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: { description: string; name: string; id?: string };
|
||||||
|
isActionVisible?: boolean;
|
||||||
|
isIconVisible?: boolean;
|
||||||
|
isDataset?: boolean;
|
||||||
|
isCheckBoxes?: boolean;
|
||||||
|
onSelect?: (value: string) => void;
|
||||||
|
onRemove?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCard = ({
|
||||||
|
item,
|
||||||
|
isActionVisible = false,
|
||||||
|
isIconVisible = false,
|
||||||
|
isDataset = false,
|
||||||
|
isCheckBoxes = false,
|
||||||
|
onSelect,
|
||||||
|
onRemove,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="tw-card tw-flex tw-justify-between tw-py-2 tw-px-3 tw-group">
|
||||||
|
<div className={`tw-flex ${isCheckBoxes ? 'tw-mr-2' : 'tw-gap-1'}`}>
|
||||||
|
{isIconVisible ? <Avatar name={item.description} /> : null}
|
||||||
|
|
||||||
|
<div className="tw-flex tw-flex-col tw-pl-2">
|
||||||
|
{isDataset ? (
|
||||||
|
<Link to={`/dataset/${item.description}`}>
|
||||||
|
<button className="tw-font-normal tw-text-grey-body">
|
||||||
|
{getPartialNameFromFQN(item.description, ['database', 'table'])}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<p className="tw-font-normal">{item.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p>{isIconVisible ? item.name : capitalize(item.name)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isActionVisible && (
|
||||||
|
<div className="tw-flex-none">
|
||||||
|
{isCheckBoxes ? (
|
||||||
|
<input
|
||||||
|
className="tw-px-2 custom-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={() => {
|
||||||
|
onSelect?.(item.id as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span onClick={() => onRemove?.(item.id as string)}>
|
||||||
|
<SVGIcons
|
||||||
|
alt="delete"
|
||||||
|
className="tw-text-gray-500 tw-cursor-pointer tw-opacity-0 hover:tw-text-gray-700 group-hover:tw-opacity-100"
|
||||||
|
icon="icon-delete"
|
||||||
|
title="Remove"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserCard;
|
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PageContainer from '../../components/containers/PageContainer';
|
|
||||||
|
|
||||||
const TeamsPage = () => {
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<h1>Teams</h1>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsPage;
|
|
@ -0,0 +1,422 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Team, User, UserTeam } from 'Models';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import AppState from '../../AppState';
|
||||||
|
import {
|
||||||
|
createTeam,
|
||||||
|
getTeamByName,
|
||||||
|
getTeams,
|
||||||
|
patchTeamDetail,
|
||||||
|
} from '../../axiosAPIs/teamsAPI';
|
||||||
|
import { Button } from '../../components/buttons/Button/Button';
|
||||||
|
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
|
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
|
||||||
|
import PageContainer from '../../components/containers/PageContainer';
|
||||||
|
import Loader from '../../components/Loader/Loader';
|
||||||
|
import FormModal from '../../components/Modals/FormModal';
|
||||||
|
import { ModalWithMarkdownEditor } from '../../components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||||
|
import { ERROR404 } from '../../constants/constants';
|
||||||
|
import SVGIcons from '../../utils/SvgUtils';
|
||||||
|
import AddUsersModal from './AddUsersModal';
|
||||||
|
import Form from './Form';
|
||||||
|
import UserCard from './UserCard';
|
||||||
|
|
||||||
|
const TeamsPage = () => {
|
||||||
|
const [teams, setTeams] = useState<Array<Team>>([]);
|
||||||
|
const [currentTeam, setCurrentTeam] = useState<Team>();
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [currentTab, setCurrentTab] = useState<number>(1);
|
||||||
|
const [isEditable, setIsEditable] = useState<boolean>(false);
|
||||||
|
const [isAddingTeam, setIsAddingTeam] = useState<boolean>(false);
|
||||||
|
const [isAddingUsers, setIsAddingUsers] = useState<boolean>(false);
|
||||||
|
const [userList, setUserList] = useState<Array<User>>([]);
|
||||||
|
|
||||||
|
const fetchTeams = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
getTeams(['users', 'owns'])
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
setTeams(res.data.data);
|
||||||
|
setCurrentTeam(res.data.data[0]);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
if (err?.response?.data.code) {
|
||||||
|
setError(ERROR404);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCurrentTeam = (name: string, update = false) => {
|
||||||
|
if (currentTeam?.name !== name || update) {
|
||||||
|
setIsLoading(true);
|
||||||
|
getTeamByName(name, ['users', 'owns'])
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
setCurrentTeam(res.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
if (err?.response?.data.code) {
|
||||||
|
setError(ERROR404);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewTeam = (data: Team) => {
|
||||||
|
createTeam(data)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
fetchTeams();
|
||||||
|
setIsAddingTeam(false);
|
||||||
|
} else {
|
||||||
|
setIsAddingTeam(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsAddingTeam(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUsers = (data: Array<UserTeam>) => {
|
||||||
|
const updatedTeam = {
|
||||||
|
...currentTeam,
|
||||||
|
users: [...(currentTeam?.users as Array<UserTeam>), ...data],
|
||||||
|
};
|
||||||
|
const jsonPatch = compare(currentTeam as Team, updatedTeam);
|
||||||
|
patchTeamDetail(currentTeam?.id, jsonPatch).then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
fetchCurrentTeam(res.data.name, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setIsAddingUsers(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = (id: string) => {
|
||||||
|
const users = [...(currentTeam?.users as Array<UserTeam>)];
|
||||||
|
const newUsers = users.filter((user) => {
|
||||||
|
return user.id !== id;
|
||||||
|
});
|
||||||
|
const updatedTeam = {
|
||||||
|
...currentTeam,
|
||||||
|
users: newUsers,
|
||||||
|
};
|
||||||
|
const jsonPatch = compare(currentTeam as Team, updatedTeam);
|
||||||
|
patchTeamDetail(currentTeam?.id, jsonPatch).then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
fetchCurrentTeam(res.data.name, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentTeamClass = (name: string) => {
|
||||||
|
if (currentTeam?.name === name) {
|
||||||
|
return 'activeCategory';
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getActiveTabClass = (tab: number) => {
|
||||||
|
return tab === currentTab ? 'active' : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabs = () => {
|
||||||
|
return (
|
||||||
|
<div className="tw-mb-3 ">
|
||||||
|
<nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4">
|
||||||
|
<button
|
||||||
|
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(1)}`}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentTab(1);
|
||||||
|
}}>
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(2)}`}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentTab(2);
|
||||||
|
}}>
|
||||||
|
Assets
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserCards = () => {
|
||||||
|
if ((currentTeam?.users.length as number) <= 0) {
|
||||||
|
return (
|
||||||
|
<div className="tw-flex tw-flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1">
|
||||||
|
<p>there are not any users added yet.</p>
|
||||||
|
<p>would like to start adding some ?</p>
|
||||||
|
<Button
|
||||||
|
className="tw-h-8 tw-rounded tw-mb-2"
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setIsAddingUsers(true)}>
|
||||||
|
Add new user
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="tw-grid xl:tw-grid-cols-4 md:tw-grid-cols-2 tw-gap-4">
|
||||||
|
{currentTeam?.users.map((user, index) => {
|
||||||
|
const User = {
|
||||||
|
description: user.description,
|
||||||
|
name: user.name,
|
||||||
|
id: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserCard
|
||||||
|
isIconVisible
|
||||||
|
item={User}
|
||||||
|
key={index}
|
||||||
|
onRemove={deleteUser}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatasetCards = () => {
|
||||||
|
if ((currentTeam?.owns.length as number) <= 0) {
|
||||||
|
return (
|
||||||
|
<div className="tw-flex tw-flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1">
|
||||||
|
<p>Your team does not have any dataset</p>
|
||||||
|
<p>would like to start adding some ?</p>
|
||||||
|
<Link to="/explore">
|
||||||
|
<Button
|
||||||
|
className="tw-h-8 tw-rounded tw-mb-2 tw-text-white"
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained">
|
||||||
|
Explore
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="tw-grid xl:tw-grid-cols-4 md:tw-grid-cols-2 tw-gap-4">
|
||||||
|
{' '}
|
||||||
|
{currentTeam?.owns.map((dataset, index) => {
|
||||||
|
const Dataset = { description: dataset.name, name: dataset.type };
|
||||||
|
|
||||||
|
return <UserCard isDataset item={Dataset} key={index} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const fetchLeftPanel = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="tw-flex tw-justify-between tw-items-baseline tw-mb-3 tw-border-b">
|
||||||
|
<h6 className="tw-heading">Teams</h6>
|
||||||
|
<Button
|
||||||
|
className="tw-h-7 tw-px-2"
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setIsAddingTeam(true)}>
|
||||||
|
<i aria-hidden="true" className="fa fa-plus" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{teams &&
|
||||||
|
teams.map((team: Team) => (
|
||||||
|
<div
|
||||||
|
className={`tw-group tw-text-grey-body tw-cursor-pointer tw-text-body tw-mb-3 tw-flex tw-justify-between ${getCurrentTeamClass(
|
||||||
|
team.name
|
||||||
|
)}`}
|
||||||
|
key={team.name}
|
||||||
|
onClick={() => {
|
||||||
|
fetchCurrentTeam(team.name);
|
||||||
|
setCurrentTab(1);
|
||||||
|
}}>
|
||||||
|
<p className="tw-text-center tag-category tw-self-center">
|
||||||
|
{team.displayName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onDescriptionUpdate = (updatedHTML: string) => {
|
||||||
|
if (currentTeam?.description !== updatedHTML) {
|
||||||
|
const updatedTeam = { ...currentTeam, description: updatedHTML };
|
||||||
|
const jsonPatch = compare(currentTeam as Team, updatedTeam);
|
||||||
|
patchTeamDetail(currentTeam?.id, jsonPatch).then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
fetchCurrentTeam(res.data.name, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsEditable(false);
|
||||||
|
} else {
|
||||||
|
setIsEditable(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onDescriptionEdit = (): void => {
|
||||||
|
setIsEditable(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = (): void => {
|
||||||
|
setIsEditable(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUniqueUserList = () => {
|
||||||
|
const uniqueList = userList
|
||||||
|
.filter((user) => {
|
||||||
|
const teamUser = currentTeam?.users.some(
|
||||||
|
(teamUser) => user.id === teamUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return !teamUser && user;
|
||||||
|
})
|
||||||
|
.map((user) => {
|
||||||
|
return {
|
||||||
|
description: user.displayName,
|
||||||
|
id: user.id,
|
||||||
|
href: user.href,
|
||||||
|
name: user.name,
|
||||||
|
type: 'user',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueList;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTeams();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUserList(AppState.users);
|
||||||
|
}, [AppState.users]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error ? (
|
||||||
|
<ErrorPlaceHolder />
|
||||||
|
) : (
|
||||||
|
<PageContainer leftPanelContent={fetchLeftPanel()}>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<div className="container-fluid tw-pt-1 tw-pb-3">
|
||||||
|
<div className="tw-flex tw-justify-between tw-pl-1">
|
||||||
|
<div className="tw-heading tw-text-link tw-text-base">
|
||||||
|
{currentTeam?.displayName}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="tw-h-8 tw-rounded tw-mb-2"
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setIsAddingUsers(true)}>
|
||||||
|
Add new user
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex tw-flex-col tw-border tw-rounded-md tw-mb-3 tw-min-h-32 tw-bg-white">
|
||||||
|
<div className="tw-flex tw-items-center tw-px-3 tw-py-1 tw-border-b">
|
||||||
|
<span className="tw-flex-1 tw-leading-8 tw-m-0 tw-font-normal">
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
<div className="tw-flex-initial">
|
||||||
|
<button
|
||||||
|
className="focus:tw-outline-none"
|
||||||
|
onClick={onDescriptionEdit}>
|
||||||
|
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tw-px-3 tw-pl-5 tw-py-2 tw-overflow-y-auto">
|
||||||
|
<div data-testid="description" id="description">
|
||||||
|
{currentTeam?.description.trim() ? (
|
||||||
|
<RichTextEditorPreviewer
|
||||||
|
markdown={currentTeam.description}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="tw-no-description">
|
||||||
|
No description added
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isEditable && (
|
||||||
|
<ModalWithMarkdownEditor
|
||||||
|
header={`Edit description for ${currentTeam?.displayName}`}
|
||||||
|
placeholder="Enter Description"
|
||||||
|
value={currentTeam?.description || ''}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSave={onDescriptionUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getTabs()}
|
||||||
|
|
||||||
|
{currentTab === 1 && getUserCards()}
|
||||||
|
|
||||||
|
{currentTab === 2 && getDatasetCards()}
|
||||||
|
|
||||||
|
{isAddingTeam && (
|
||||||
|
<FormModal
|
||||||
|
form={Form}
|
||||||
|
header="Adding new team"
|
||||||
|
initialData={{
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
displayName: '',
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsAddingTeam(false)}
|
||||||
|
onSave={(data) => createNewTeam(data as Team)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isAddingUsers && (
|
||||||
|
<AddUsersModal
|
||||||
|
header={`Adding new users to ${currentTeam?.name}`}
|
||||||
|
list={getUniqueUserList()}
|
||||||
|
onCancel={() => setIsAddingUsers(false)}
|
||||||
|
onSave={(data) => createUsers(data)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(TeamsPage);
|
@ -405,6 +405,13 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
.side-panel .activeCategory {
|
||||||
|
margin-right: -15px;
|
||||||
|
margin-left: -15px;
|
||||||
|
padding: 0px 15px;
|
||||||
|
border-right: 3px solid rgba(249, 130, 108);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.search-wrapper + .page-container .side-panel {
|
.search-wrapper + .page-container .side-panel {
|
||||||
top: 102px;
|
top: 102px;
|
||||||
|
@ -49,7 +49,7 @@ export const getPartialNameFromFQN = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return arrPartialName.join('.');
|
return arrPartialName.join('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentUserId = (): string => {
|
export const getCurrentUserId = (): string => {
|
||||||
|
@ -98,6 +98,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
32: '8rem',
|
32: '8rem',
|
||||||
|
'90vh': '90vh',
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
32: '8rem',
|
32: '8rem',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user