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 8 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
3 changes: 3 additions & 0 deletions server/constants/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ const (
AlreadyLinkedProject = "This project is already linked."
GetProjectListError = "Error getting Project List"
ErrorFetchProjectList = "Error in fetching project list"
ErrorDecodingBody = "Error in decoding body"
ProjectNotFound = "Requested project does not exists"
ErrorUnlinkProject = "Error in unlinking the project"
)
12 changes: 7 additions & 5 deletions server/constants/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package constants

const (
// Plugin API Routes
APIPrefix = "/api/v1"
WildRoute = "{anything:.*}"
PathOAuthConnect = "/oauth/connect"
PathOAuthCallback = "/oauth/complete"
PathGetAllLinkedProjects = "/link/project"
APIPrefix = "/api/v1"
WildRoute = "{anything:.*}"
PathOAuthConnect = "/oauth/connect"
PathOAuthCallback = "/oauth/complete"
PathLinkedProjects = "/project/link"
PathUnlinkProject = "/project/unlink"
PathUser = "/user"
)
99 changes: 92 additions & 7 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ func (p *Plugin) InitRoutes() {
// Plugin APIs
s.HandleFunc("/tasks", p.handleAuthRequired(p.handleCreateTask)).Methods(http.MethodPost)
s.HandleFunc("/link", p.handleAuthRequired(p.handleLink)).Methods(http.MethodPost)
s.HandleFunc(constants.PathGetAllLinkedProjects, p.handleAuthRequired(p.handleGetAllLinkedProjects)).Methods(http.MethodGet)
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 All @@ -45,7 +47,7 @@ func (p *Plugin) handleCreateTask(w http.ResponseWriter, r *http.Request) {

body, err := serializers.CreateTaskRequestPayloadFromJSON(r.Body)
if err != nil {
p.API.LogError("Error in decoding the body for creating a task", "Error", err.Error())
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: err.Error()})
return
}
Expand Down Expand Up @@ -83,7 +85,7 @@ func (p *Plugin) handleLink(w http.ResponseWriter, r *http.Request) {
var body *serializers.LinkRequestPayload
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&body); err != nil {
p.API.LogError("Error in decoding body", "Error", err.Error())
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
Expand Down Expand Up @@ -136,16 +138,21 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque
return
}

w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

if projectList == nil {
_, _ = w.Write([]byte("[]"))
return
}

response, err := json.Marshal(projectList)
if err != nil {
p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error())
error := serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}
p.handleError(w, r, &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 {
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
}
Expand Down Expand Up @@ -178,6 +185,84 @@ func (p *Plugin) handleError(w http.ResponseWriter, r *http.Request, error *seri
}
}

// handleUnlinkProject unlinks a project
func (p *Plugin) handleUnlinkProject(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)

var project *serializers.ProjectDetails
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&project); err != nil {
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: err.Error()})
return
}

projectList, err := p.Store.GetAllProjects(mattermostUserID)
if err != nil {
p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}

if !p.IsProjectLinked(projectList, *project) {
p.API.LogError(constants.ProjectNotFound, "Error")
p.handleError(w, r, &serializers.Error{Code: http.StatusNotFound, Message: constants.ProjectNotFound})
return
}

if err := p.Store.DeleteProject(project); err != nil {
p.API.LogError(constants.ErrorUnlinkProject, "Error", err.Error())
p.handleError(w, r, &serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()})
}

successResponse := &serializers.SuccessResponse{
Message: "success",
}
response, err := json.Marshal(&successResponse)
if err != nil {
p.API.LogError("Error marhsalling 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)
}
}

// handleUnlinkProject unlinks a project
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
func (p *Plugin) handleGetUserAccountDetails(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get(constants.HeaderMattermostUserIDAPI)

avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
userDetails, err := p.Store.LoadUser(mattermostUserID)
if err != nil {
p.API.LogError(constants.ErrorDecodingBody, "Error", err.Error())
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
p.handleError(w, r, &serializers.Error{Code: http.StatusBadRequest, Message: err.Error()})
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
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 marhsalling response", "Error", err.Error())
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
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
4 changes: 4 additions & 0 deletions server/serializers/error.go → server/serializers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ type Error struct {
Code int
Message string
}

type SuccessResponse struct {
Message string `json:"message"`
}
3 changes: 2 additions & 1 deletion webapp/src/components/buttons/iconButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const IconButton = ({tooltipText, iconClassName, extraClass = '', iconColor, onC
<Tooltip tooltipContent={tooltipText}>
<Button
variant='outline-danger'
className={`button-wrapper ${extraClass} ${iconColor === 'danger' && 'danger'}`}
className={`plugin-btn button-wrapper btn-icon ${extraClass}`}
onClick={onClick}
>
<i
className={iconClassName}
Expand Down
16 changes: 8 additions & 8 deletions webapp/src/components/card/project/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@ import IconButton from 'components/buttons/iconButton';

import {onPressingEnterKey} from 'utils';

import './styles.scss';

type ProjectCardProps = {
onProjectTitleClick: (projectDetails: ProjectDetails) => void;
onProjectTitleClick: (projectDetails: ProjectDetails) => void
handleUnlinkProject: (projectDetails: ProjectDetails) => void
projectDetails: ProjectDetails
}

const ProjectCard = ({onProjectTitleClick, projectDetails: {organization, title}, projectDetails}: ProjectCardProps) => {
const ProjectCard = ({onProjectTitleClick, projectDetails: {organizationName, projectName}, projectDetails, handleUnlinkProject}: ProjectCardProps) => {
return (
<BaseCard>
<div className='d-flex'>
<div className='project-details'>
<p className='margin-bottom-10'>
<span
aria-label={title}
aria-label={projectName}
role='button'
tabIndex={0}
className='font-size-14 font-bold link-title'
onKeyDown={() => onPressingEnterKey(event, () => onProjectTitleClick(projectDetails))}
onClick={() => onProjectTitleClick(projectDetails)}
>
{title}
{projectName}
</span>
</p>
<p className='font-size-14'>{organization}</p>
<p className='font-size-14'>{organizationName}</p>
</div>
<div className='button-wrapper'>
<IconButton
tooltipText='Unlink project'
iconClassName='fa fa-chain-broken'
extraClass='project-details-unlink-button unlink-button'
extraClass='unlink-button'
onClick={() => handleUnlinkProject(projectDetails)}
/>
</div>
</div>
Expand Down
16 changes: 0 additions & 16 deletions webapp/src/components/card/project/styles.scss

This file was deleted.

8 changes: 0 additions & 8 deletions webapp/src/components/card/subscription/styles.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
.project-details {
flex-basis: 70%;
}

.button-wrapper {
flex-basis: 30%;
}

.delete-button {
display: block;
margin-left: auto;
Expand Down
92 changes: 92 additions & 0 deletions webapp/src/components/emptyState/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';

import './styles.scss';

type DisplayIcon = 'folder' | 'azure'

type EmptyStatePropTypes = {
title: string,
subTitle?: {
text: string
slashCommand?: string
},
buttonText?: string,
buttonAction?: (event: React.SyntheticEvent) => void;
icon?: DisplayIcon;
}

// TODO: UI to be changed
const EmptyState = ({title, subTitle, buttonText, buttonAction, icon = 'folder'}: EmptyStatePropTypes) => {
return (
<div className='no-data d-flex'>
<div className='d-flex flex-column align-items-center'>
<div className='no-data__icon d-flex justify-content-center align-items-center'>
{
icon === 'azure' && (
<svg
width='36'
height='36'
viewBox='0 0 36 36'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M4.449 12.4965L27.033 8.1045L15.978 0V3.7155L3.3705 8.8485L0 13.3065V23.571L4.449 24.855V12.4965ZM13.416 31.407L27.3705 36L36 28.638V6.618L27.0345 8.1045V27.5565L4.449 24.855L13.416 36V31.407Z'
fill='#8E8E8E'
/>
</svg>
)
}
{
icon === 'folder' && (
<svg
width='48'
height='40'
viewBox='0 0 48 40'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M2 21.1112V35.2223C2 36.8792 3.34314 38.2223 5 38.2223H36.3333C37.9902 38.2223 39.3333 36.8792 39.3333 35.2223V21.1112M2 21.1112V11.6667C2 10.0099 3.34315 8.66675 5 8.66675H13.5361C14.1284 8.66675 14.7074 8.84206 15.2002 9.1706L20.6887 12.8296C21.1815 13.1581 21.7605 13.3334 22.3528 13.3334H36.3333C37.9902 13.3334 39.3333 14.6766 39.3333 16.3334V21.1112M2 21.1112H39.3333'
stroke='#8E8E8E'
strokeWidth='3.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M9.77783 2H16.9917C18.3737 2 19.7248 2.40907 20.8746 3.17565L24.3477 5.49102C25.4976 6.2576 26.8486 6.66667 28.2306 6.66667H39.0001C42.8661 6.66667 46.0001 9.80067 46.0001 13.6667V28.4444'
stroke='#8E8E8E'
strokeWidth='3.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
</div>
<p className='no-data__title'>{title}</p>
{subTitle && (
<>
<p className='no-data__subtitle'>{subTitle.text}</p>
{
subTitle.slashCommand && <p className='slash-command'>{subTitle.slashCommand}</p>
}

</>
)}
{
buttonText && buttonAction && (
<button
onClick={buttonAction}
className='plugin-btn no-data__btn btn btn-primary'
>
{buttonText}
</button>
)
}
</div>
</div>
);
};

export default EmptyState;
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.no-data {
text-align: center;
height: 100%;
min-height: 350px;
justify-content: center;
margin-top: 80px;
text-align: center;

&__icon {
width: 120px;
height: 120px;
width: 100px;
height: 100px;
background: rgba(var(--center-channel-color-rgb), 0.04);
border-radius: 100%;
margin-bottom: 24px;
Expand All @@ -25,3 +26,9 @@
margin-top: 24px;
}
}

.slash-command {
padding: 10px 15px;
border-radius: 4px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
2 changes: 1 addition & 1 deletion webapp/src/components/labelValuePair/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type LabelValuePairProps = {
const LabelValuePair = ({label, value}: LabelValuePairProps) => {
return (
<p className='margin-bottom-10'>
<strong>{label}{': '}</strong>
<strong>{`${label}: `}</strong>
<span className='value'>{value}</span>
</p>
);
Expand Down
Loading