mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-02 21:53:30 +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.
|
||||
*/
|
||||
|
||||
import { Team } from 'Models';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { TagsCategory } from '../../../pages/tags/tagsTypes';
|
||||
import { Button } from '../../buttons/Button/Button';
|
||||
type FormData = TagsCategory | Team;
|
||||
type FormModalProp = {
|
||||
onCancel: () => void;
|
||||
onSave: (data: TagsCategory) => void;
|
||||
form: React.ElementType;
|
||||
header: string;
|
||||
initialData: TagsCategory;
|
||||
initialData: FormData;
|
||||
};
|
||||
type FormRef = {
|
||||
fetchMarkDownData: () => string;
|
||||
@ -36,7 +38,7 @@ const FormModal = ({
|
||||
initialData,
|
||||
}: FormModalProp) => {
|
||||
const formRef = useRef<FormRef>();
|
||||
const [data, setData] = useState<TagsCategory>(initialData);
|
||||
const [data, setData] = useState<FormData>(initialData);
|
||||
|
||||
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
@ -131,7 +131,27 @@ const Appbar: React.FC = (): JSX.Element => {
|
||||
/>
|
||||
</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">
|
||||
<DropDown
|
||||
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}
|
||||
/>
|
||||
</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 = [
|
||||
// { name: 'Teams', to: '/teams', disabled: false },
|
||||
{ name: 'Teams', to: '/teams', disabled: false },
|
||||
{ name: 'Tags', to: '/tags', disabled: false },
|
||||
// { name: 'Store', to: '/store', disabled: false },
|
||||
{ name: 'Services', to: '/services', disabled: false },
|
||||
|
@ -36,5 +36,7 @@ export const useAuth = (pathname = '') => {
|
||||
isEmpty(userDetails) &&
|
||||
isEmpty(newUser),
|
||||
isAuthenticatedRoute: isAuthenticatedRoute,
|
||||
isAuthDisabled: authDisabled,
|
||||
isAdminUser: userDetails?.isAdmin,
|
||||
};
|
||||
};
|
||||
|
@ -168,8 +168,8 @@ declare module 'Models' {
|
||||
};
|
||||
|
||||
export type UserTeam = {
|
||||
description?: string;
|
||||
href?: string;
|
||||
description: string;
|
||||
href: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
@ -178,11 +178,13 @@ declare module 'Models' {
|
||||
export type User = {
|
||||
displayName: string;
|
||||
isBot: boolean;
|
||||
isAdmin: boolean;
|
||||
id: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
profile: UserProfile;
|
||||
teams: Array<UserTeam>;
|
||||
timezone: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type FormatedTableData = {
|
||||
@ -281,7 +283,15 @@ declare module 'Models' {
|
||||
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 = {
|
||||
name: string;
|
||||
value: string;
|
||||
|
@ -161,7 +161,7 @@ const TagsPage = () => {
|
||||
const fetchLeftPanel = () => {
|
||||
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>
|
||||
<Button
|
||||
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-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 {
|
||||
top: 102px;
|
||||
|
@ -49,7 +49,7 @@ export const getPartialNameFromFQN = (
|
||||
}
|
||||
}
|
||||
|
||||
return arrPartialName.join('.');
|
||||
return arrPartialName.join('/');
|
||||
};
|
||||
|
||||
export const getCurrentUserId = (): string => {
|
||||
|
@ -98,6 +98,7 @@ module.exports = {
|
||||
},
|
||||
maxHeight: {
|
||||
32: '8rem',
|
||||
'90vh': '90vh',
|
||||
},
|
||||
minHeight: {
|
||||
32: '8rem',
|
||||
|
Loading…
x
Reference in New Issue
Block a user