Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[MI-2002]: Created plugin API to fetch user details and UI integratio… #30

Merged
merged 19 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/constants/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
GetProjectListError = "Error getting Project List"
ErrorFetchProjectList = "Error in fetching project list"
ErrorDecodingBody = "Error in decoding body"
ErrorLoadingDataFromKVStore = "Error in loading data from KV store"
ProjectNotFound = "Requested project does not exist"
ErrorUnlinkProject = "Error in unlinking the project"
)
5 changes: 3 additions & 2 deletions server/constants/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const (
WildRoute = "{anything:.*}"
PathOAuthConnect = "/oauth/connect"
PathOAuthCallback = "/oauth/complete"
PathLinkedProjects = "/link/project"
PathUnlinkProject = "/unlink/project"
PathLinkedProjects = "/project/link"
PathUnlinkProject = "/project/unlink"
PathUser = "/user"
)
31 changes: 31 additions & 0 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (p *Plugin) InitRoutes() {
s.HandleFunc("/link", p.handleAuthRequired(p.handleLink)).Methods(http.MethodPost)
s.HandleFunc(constants.PathLinkedProjects, p.handleAuthRequired(p.handleGetAllLinkedProjects)).Methods(http.MethodGet)
s.HandleFunc(constants.PathUnlinkProject, p.handleAuthRequired(p.handleUnlinkProject)).Methods(http.MethodPost)
s.HandleFunc(constants.PathUser, p.handleAuthRequired(p.handleGetUserAccountDetails)).Methods(http.MethodGet)
}

// API to create task of a project in an organization.
Expand Down Expand Up @@ -233,6 +234,36 @@ func (p *Plugin) handleUnlinkProject(w http.ResponseWriter, r *http.Request) {
}
}

// handleGetUserAccountDetails provides user details
func (p *Plugin) handleGetUserAccountDetails(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)
userDetails, err := p.Store.LoadUser(mattermostUserID)
if err != nil {
p.API.LogError(constants.ErrorLoadingDataFromKVStore, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}

if userDetails.MattermostUserID == "" {
p.API.LogError(constants.ConnectAccountFirst, "Error")
p.handleError(w, r, &serializers.Error{Code: http.StatusUnauthorized, Message: constants.ConnectAccountFirst})
return
}

response, err := json.Marshal(&userDetails)
if err != nil {
p.API.LogError("Error marshaling the response", "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}

w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (p *Plugin) WithRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/components/emptyState/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';

import './styles.scss';

type DisplayIcon = 'folder' | 'azure'

type EmptyStatePropTypes = {
title: string,
subTitle?: {
Expand All @@ -10,6 +12,7 @@ type EmptyStatePropTypes = {
},
buttonText?: string,
buttonAction?: (event: React.SyntheticEvent) => void;
icon?: DisplayIcon;
}

const EmptyState = ({title, subTitle, buttonText, buttonAction}: EmptyStatePropTypes) => (
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/components/emptyState/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@
border-radius: 4px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}

.slash-command {
padding: 10px 15px;
border-radius: 4px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
38 changes: 38 additions & 0 deletions webapp/src/containers/Rhs/accountNotLinked/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import {useDispatch} from 'react-redux';

import EmptyState from 'components/emptyState';

import Utils from 'utils';

const AccountNotLinked = () => {
const dispatch = useDispatch();

const closeRHS = () => {
dispatch({
type: 'UPDATE_RHS_STATE',
state: null,
});
};

// Opens link project modal
const handleConnectAccount = () => {
window.open(`${Utils.getBaseUrls().pluginApiBaseUrl}/oauth/connect`, '_blank');
closeRHS();
};

return (
<>
<EmptyState
title='No account connected'
subTitle={{text: 'Connect your account by clicking the button below'}}
buttonText='Connect your account'
buttonAction={handleConnectAccount}
icon='azure'
/>
</>
);
};

export default AccountNotLinked;
34 changes: 27 additions & 7 deletions webapp/src/containers/Rhs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import React from 'react';
import React, {useEffect} from 'react';

import usePluginApi from 'hooks/usePluginApi';
import {getprojectDetailsState, getRhsState} from 'selectors';

import LinearLoader from 'components/loader/linear';

import plugin_constants from 'plugin_constants';

import AccountNotLinked from './accountNotLinked';
import ProjectList from './projectList';
import ProjectDetails from './projectDetails';

const Rhs = (): JSX.Element => {
const usePlugin = usePluginApi();
const {getApiState, makeApiRequest, state} = usePluginApi();

// Fetch the connected account details when RHS is opened
useEffect(() => {
if (getRhsState(state).isSidebarOpen) {
makeApiRequest(plugin_constants.pluginApiServiceConfigs.getUserDetails.apiServiceName);
}
}, []);

const {isLoading, isError, isSuccess} = getApiState(plugin_constants.pluginApiServiceConfigs.getUserDetails.apiServiceName);

if (!getRhsState(usePlugin.state).isSidebarOpen) {
if (!getRhsState(state).isSidebarOpen) {
return <></>;
}

return (
<div className='height-rhs bg-sidebar padding-25'>
<div className='overflow-auto height-rhs bg-sidebar padding-25'>
{isLoading && <LinearLoader/>}
{isError && <AccountNotLinked/>}
{
getprojectDetailsState(usePlugin.state).projectID ?
<ProjectDetails title={getprojectDetailsState(usePlugin.state).projectName}/> :
<ProjectList/>
!isLoading &&
!isError &&
isSuccess && (
getprojectDetailsState(state).projectID ?
<ProjectDetails title={getprojectDetailsState(state).projectName}/> :
<ProjectList/>
)
}
</div>
);
Expand Down
13 changes: 4 additions & 9 deletions webapp/src/containers/Rhs/projectList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const ProjectList = () => {
}
}, [getLinkModalState(usePlugin.state)]);

const {data} = usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName);
const {data, isSuccess, isLoading} = usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName);

return (
<>
Expand All @@ -76,20 +76,15 @@ const ProjectList = () => {
isOpen={showConfirmationModal}
onHide={() => setShowConfirmationModal(false)}
onConfirm={handleConfirmUnlinkProject}
isLoading={usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.unlinkProject.apiServiceName, projectToBeUnlinked).isLoading}
isLoading={isLoading}
confirmBtnText='Unlink'
description={`Are you sure you want to unlink ${projectToBeUnlinked?.projectName}?`}
title='Confirm Project Unlink'
/>
}
{isLoading && <LinearLoader/>}
{
usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName).isLoading && (
<LinearLoader/>
)
}
{
usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName).isSuccess &&
data &&
isSuccess && data &&
(
data.length ?
data.map((item) => (
Expand Down
22 changes: 21 additions & 1 deletion webapp/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export default class Hooks {
this.store = store;
}

closeRhs() {
this.store.dispatch({
type: 'UPDATE_RHS_STATE',
state: null,
});
}

slashCommandWillBePostedHook = (message, contextArgs) => {
let commandTrimmed;
if (message) {
Expand All @@ -19,7 +26,6 @@ export default class Hooks {
args: contextArgs,
});
}

if (commandTrimmed && commandTrimmed.startsWith('/azuredevops boards create')) {
const args = splitArgs(commandTrimmed);
this.store.dispatch(showTaskModal(args));
Expand All @@ -30,6 +36,20 @@ export default class Hooks {
this.store.dispatch(showLinkModal(args));
return Promise.resolve({});
}
if (commandTrimmed && commandTrimmed.startsWith('/azuredevops connect')) {
this.closeRhs();
return {
message,
args: contextArgs,
};
}
if (commandTrimmed && commandTrimmed.startsWith('/azuredevops disconnect')) {
this.closeRhs();
return {
message,
args: contextArgs,
};
}
return Promise.resolve({
message,
args: contextArgs,
Expand Down
9 changes: 7 additions & 2 deletions webapp/src/plugin_constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,20 @@ const pluginApiServiceConfigs: Record<ApiServiceName, PluginApiService> = {
apiServiceName: 'testGet',
},
getAllLinkedProjectsList: {
path: '/link/project',
path: '/project/link',
method: 'GET',
apiServiceName: 'getAllLinkedProjectsList',
},
unlinkProject: {
path: '/unlink/project',
path: '/project/unlink',
method: 'POST',
apiServiceName: 'unlinkProject',
},
getUserDetails: {
path: '/user',
method: 'GET',
apiServiceName: 'getUserDetails',
},
};

export default {
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ const pluginApi = createApi({
body: payload,
}),
}),
[Constants.pluginApiServiceConfigs.getUserDetails.apiServiceName]: builder.query<UserDetails, void>({
query: () => ({
headers: {[Constants.HeaderMattermostUserID]: Cookies.get(Constants.MMUSERID)},
url: Constants.pluginApiServiceConfigs.getUserDetails.path,
method: Constants.pluginApiServiceConfigs.getUserDetails.method,
}),
}),
}),
});

Expand Down
15 changes: 15 additions & 0 deletions webapp/src/styles/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@
margin: 6px 5px 0 0;
}
}

.rhs-project-list-wrapper {
position: fixed;
bottom: 0;
left: 0;
padding: 15px 0 30px;
width: 100%;
background-color: var(--center-channel-bg);
box-shadow: 5px -1px 5px rgba(0, 0, 0, 0.1);
text-align: center;

.project-list-btn {
width: calc(100% - 60px); // 60px is the width of padding from left and right
}
}
5 changes: 5 additions & 0 deletions webapp/src/styles/_utils.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@
.cursor-pointer {
cursor: pointer;
}

// Overflows
.overflow-auto {
overflow: auto;
}
8 changes: 6 additions & 2 deletions webapp/src/types/common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

type HttpMethod = 'GET' | 'POST';

type ApiServiceName = 'createTask' | 'testGet' | 'createLink' | 'getAllLinkedProjectsList' | 'unlinkProject'
type ApiServiceName = 'createTask' | 'testGet' | 'createLink' | 'getAllLinkedProjectsList' | 'unlinkProject' | 'getUserDetails'

type PluginApiService = {
path: string,
Expand Down Expand Up @@ -43,7 +43,7 @@ type CreateTaskPayload = {
fields: CreateTaskFields,
}

type APIRequestPayload = CreateTaskPayload | LinkPayload | ProjectDetails | void;
type APIRequestPayload = CreateTaskPayload | LinkPayload | ProjectDetails | UserDetails | void;

type DropdownOptionType = {
label?: string | JSX.Element;
Expand All @@ -62,6 +62,10 @@ type ProjectDetails = {
organizationName: string
}

type UserDetails = {
MattermostUserID: string
}

type eventType = 'create' | 'update' | 'delete'

type SubscriptionDetails = {
Expand Down