diff --git a/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts new file mode 100644 index 00000000000..ca5a538972c --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/teamsAPI.ts @@ -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 => { + const url = getURLWithQueryFields('/teams', arrQueryFields); + + return APIClient.get(`${url}&limit=1000000`); +}; + +export const getTeamByName: Function = ( + name: string, + arrQueryFields?: string +): Promise => { + 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); +}; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Modals/FormModal/index.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Modals/FormModal/index.tsx index 852c5183100..296cb309491 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/Modals/FormModal/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/Modals/FormModal/index.tsx @@ -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(); - const [data, setData] = useState(initialData); + const [data, setData] = useState(initialData); const onSubmitHandler = (e: React.FormEvent) => { e.preventDefault(); diff --git a/catalog-rest-service/src/main/resources/ui/src/components/app-bar/Appbar.tsx b/catalog-rest-service/src/main/resources/ui/src/components/app-bar/Appbar.tsx index 433dfadd735..6fdc3abf978 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/app-bar/Appbar.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/app-bar/Appbar.tsx @@ -131,7 +131,27 @@ const Appbar: React.FC = (): JSX.Element => { /> - + + {/* */} + Docs + + + {/* */} + API +
{ + 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 ( +
+

{name[0]}

+
+ ); +}; + +export default Avatar; diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx index 586962250b1..6ae7d5dddea 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx @@ -131,7 +131,9 @@ const RichTextEditor = forwardRef( onEditorStateChange={onEditorStateChange} />
-

Using headings in markdown is not allowed

+

+ Using headings in markdown is not allowed +

); } diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts index 2a4d2c230ff..6362dcfb297 100644 --- a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts +++ b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts @@ -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 }, diff --git a/catalog-rest-service/src/main/resources/ui/src/hooks/authHooks.ts b/catalog-rest-service/src/main/resources/ui/src/hooks/authHooks.ts index cf557cbb683..bea2e0e2bd0 100644 --- a/catalog-rest-service/src/main/resources/ui/src/hooks/authHooks.ts +++ b/catalog-rest-service/src/main/resources/ui/src/hooks/authHooks.ts @@ -36,5 +36,7 @@ export const useAuth = (pathname = '') => { isEmpty(userDetails) && isEmpty(newUser), isAuthenticatedRoute: isAuthenticatedRoute, + isAuthDisabled: authDisabled, + isAdminUser: userDetails?.isAdmin, }; }; diff --git a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts index 0f37be79829..cc66b6db399 100644 --- a/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts +++ b/catalog-rest-service/src/main/resources/ui/src/interface/types.d.ts @@ -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; timezone: string; + href: string; }; export type FormatedTableData = { @@ -281,7 +283,15 @@ declare module 'Models' { aggregations: Record; }; }; - + export type Team = { + id: string; + name: string; + displayName: string; + description: string; + href: string; + users: Array; + owns: Array; + }; export type ServiceCollection = { name: string; value: string; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx index 4958382a373..117d0e0bae3 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/tags/index.tsx @@ -161,7 +161,7 @@ const TagsPage = () => { const fetchLeftPanel = () => { return ( <> -
+
Tag Categories
+ +
+
+ + ); +}; + +export default AddUsersModal; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/teams/Form.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/teams/Form.tsx new file mode 100644 index 00000000000..4495ef1b31b --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/pages/teams/Form.tsx @@ -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 = forwardRef( + ({ saveData, initialData }: FormProp, ref): JSX.Element => { + const [data, setData] = useState({ + name: initialData.name, + description: initialData.description, + displayName: initialData.displayName, + id: initialData.id || '', + href: initialData.href || '', + owns: initialData.owns || [], + users: initialData.users || [], + }); + + const markdownRef = useRef(); + + const onChangeHadler = ( + e: React.ChangeEvent + ) => { + e.persist(); + setData((prevState) => { + return { + ...prevState, + [e.target.name]: e.target.value, + }; + }); + }; + useImperativeHandle(ref, () => ({ + fetchMarkDownData() { + return markdownRef.current?.getEditorContent(); + }, + })); + + useEffect(() => { + saveData({ + ...data, + }); + }, [data]); + + return ( +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); + } +); +Form.displayName = 'TeamsForm'; + +export default Form; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/teams/UserCard.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/teams/UserCard.tsx new file mode 100644 index 00000000000..65cd12a287a --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/pages/teams/UserCard.tsx @@ -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 ( +
+
+ {isIconVisible ? : null} + +
+ {isDataset ? ( + + + + ) : ( +

{item.description}

+ )} + +

{isIconVisible ? item.name : capitalize(item.name)}

+
+
+ {isActionVisible && ( +
+ {isCheckBoxes ? ( + { + onSelect?.(item.id as string); + }} + /> + ) : ( + onRemove?.(item.id as string)}> + + + )} +
+ )} +
+ ); +}; + +export default UserCard; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.jsx b/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.jsx deleted file mode 100644 index c413c68642c..00000000000 --- a/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.jsx +++ /dev/null @@ -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 ( - -

Teams

-
- ); -}; - -export default TeamsPage; diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.tsx new file mode 100644 index 00000000000..4683a150824 --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/pages/teams/index.tsx @@ -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>([]); + const [currentTeam, setCurrentTeam] = useState(); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [currentTab, setCurrentTab] = useState(1); + const [isEditable, setIsEditable] = useState(false); + const [isAddingTeam, setIsAddingTeam] = useState(false); + const [isAddingUsers, setIsAddingUsers] = useState(false); + const [userList, setUserList] = useState>([]); + + 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) => { + const updatedTeam = { + ...currentTeam, + users: [...(currentTeam?.users as Array), ...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)]; + 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 ( +
+ +
+ ); + }; + + const getUserCards = () => { + if ((currentTeam?.users.length as number) <= 0) { + return ( +
+

there are not any users added yet.

+

would like to start adding some ?

+ +
+ ); + } + + return ( + <> +
+ {currentTeam?.users.map((user, index) => { + const User = { + description: user.description, + name: user.name, + id: user.id, + }; + + return ( + + ); + })} +
+ + ); + }; + + const getDatasetCards = () => { + if ((currentTeam?.owns.length as number) <= 0) { + return ( +
+

Your team does not have any dataset

+

would like to start adding some ?

+ + + +
+ ); + } + + return ( + <> +
+ {' '} + {currentTeam?.owns.map((dataset, index) => { + const Dataset = { description: dataset.name, name: dataset.type }; + + return ; + })} +
+ + ); + }; + const fetchLeftPanel = () => { + return ( + <> +
+
Teams
+