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

Sc 12514/user details #198

Merged
merged 16 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions web/beacon-app/src/application/api/ApiAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
NewUserResponseData,
User,
} from '@/features/auth/types/RegisterService';
import { MembersResponse } from '@/features/members/types/memberServices';
import { OrgResponse } from '@/features/organizations/types/organizationService';
import type {
ProjectDetailDTO,
ProjectResponse,
Expand All @@ -22,4 +24,6 @@ export interface ApiAdapters {
projectDetail(id: ProjectDetailDTO): Promise<ProjectResponse>;
getStats(values: QuickViewDTO): Promise<any>;
getProjectList(): Promise<ProjectsResponse>;
getMemberList(): Promise<MembersResponse>;
orgDetail(orgID: string): Promise<OrgResponse>;
}
14 changes: 4 additions & 10 deletions web/beacon-app/src/components/ui/CancelModal/CancelAcctModal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { Modal } from '@rotational/beacon-core';
import { Fragment, memo, useState } from 'react';
import { Fragment } from 'react';

import { Close } from '@/components/icons/close';

function CancelAcctModal() {
const [isOpen, setIsOpen] = useState(true);

const closeModal = () => setIsOpen(false);

export default function CancelAcctModal(props: any) {
return (
<div>
<Modal title="Cancel Account" open={isOpen} containerClassName="max-w-md">
<Modal title="Cancel Account" open={true} containerClassName="max-w-md">
<Fragment key=".0">
<Close onClick={closeModal} className="absolute top-4 right-8"></Close>
<Close onClick={props.close} className="absolute top-4 right-8"></Close>
<p className="pb-4">
Please contact us at <span className="font-bold">support@rotational.io</span> to cancel
your account. Please include your name, email, and Org ID in your request to cancel your
Expand All @@ -29,5 +25,3 @@ function CancelAcctModal() {
</div>
);
}

export default memo(CancelAcctModal);
4 changes: 3 additions & 1 deletion web/beacon-app/src/constants/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ export const APP_ROUTE = {
TENANTS: '/tenant',
APIKEYS: '/apikeys',
PROJECTS: '/projects',
PROJECTS_LIST: '/{:tenantID}/projects',
PROJECTS_LIST: 'tenant/{:tenantID}/projects',
GETTING_STARTED: '/onboarding/getting-started',
ONBOARDING_SETUP: '/onboarding/setup',
MEMBERS_LIST: 'tenant/{:tenantID}/members',
ORG_DETAIL: '/organization/{:orgID}',
};

// quaterdeck api routes
Expand Down
2 changes: 2 additions & 0 deletions web/beacon-app/src/constants/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export const RQK = {
PROJECT_STATS: 'project-stats',
TENANTS_STATS: 'tenant-stats',
PROJECT_LIST: 'projectList',
MEMBER_LIST: 'memberList',
ORG_DETAIL: 'orgDetail',
} as any;
4 changes: 2 additions & 2 deletions web/beacon-app/src/features/apiKeys/types/ApiKeyServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export interface APIKey {
name: string;
owner: string;
permissions: string[];
created: string;
modified: string;
created?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are optional, so I made the correction here.

modified?: string;
}

export type NewAPIKey = Omit<APIKey, 'id'>;
29 changes: 29 additions & 0 deletions web/beacon-app/src/features/members/api/_tests_/memberList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { vi } from 'vitest';

import { memberRequest } from '../memberListAPI';

describe('Member', () => {
describe('Member List', () => {
it('returns member list request resolved with response', async () => {
const mockMembersResponse = {
PromiseRejectionEvent: [
{
id: '1',
name: 'Kamala Khan',
role: 'Owner',
},
],
};

const requestSpy = vi.fn().mockReturnValueOnce({
status: 200,
data: mockMembersResponse,
statusText: 'OK',
});
const request = memberRequest(requestSpy);
const response = await request();
expect(response).toBe(mockMembersResponse);
expect(requestSpy).toHaveBeenCalledTimes(1);
});
});
});
Copy link
Contributor

@masskoder masskoder Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add more cases but because we are running out of time we could review it in the next sprint by adding failure cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a spike story in sprint 4 about adding test cases.

15 changes: 15 additions & 0 deletions web/beacon-app/src/features/members/api/memberListAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiAdapters } from '@/application/api/ApiAdapters';
import { getValidApiResponse, Request } from '@/application/api/ApiService';
import { APP_ROUTE } from '@/constants';

import { MembersResponse } from '../types/memberServices';

export function memberRequest(request: Request): ApiAdapters['getMemberList'] {
return async () => {
const response = (await request(`${APP_ROUTE.MEMBERS_LIST}`, {
method: 'GET',
})) as any;

return getValidApiResponse<MembersResponse>(response);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Meta, Story } from '@storybook/react';

import CancelAccount from './CancelAccount';

export default {
title: 'members/CancelAccount',
component: CancelAccount,
} as Meta;

const Template: Story = (args) => <CancelAccount {...args} />;

export const Default = Template.bind({});
Default.args = {};
16 changes: 16 additions & 0 deletions web/beacon-app/src/features/members/components/CancelAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AriaButton } from '@rotational/beacon-core';
import { useState } from 'react';

import { CancelAcctModal } from '@/components/ui/CancelModal';

export default function CancelAccount(props: any) {
const [showModal, setShowModal] = useState(false);

const handleOpen = () => setShowModal(true);
return (
<AriaButton variant="tertiary" className="rounded-sm" onClick={handleOpen}>
Cancel Account
{showModal && <CancelAcctModal close={props.close} />}
</AriaButton>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Meta, Story } from '@storybook/react';

import MemberDetails from './MemberDetails';

export default {
title: 'members/MemberDetails',
component: MemberDetails,
} as Meta;

const Template: Story = (args) => <MemberDetails {...args} />;

export const Default = Template.bind({});
Default.args = {};
69 changes: 69 additions & 0 deletions web/beacon-app/src/features/members/components/MemberDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Toast } from '@rotational/beacon-core';
import { useState } from 'react';

import { BlueBars } from '@/components/icons/blueBars';

import { useFetchMembers } from '../hooks/useFetchMembers';
import CancelAccount from './CancelAccount';

export default function MemberDetails() {
const [isOpen, setIsOpen] = useState(false);

const handleToggleBars = () => {
const open = isOpen;
setIsOpen(!open);
};

const handleClose = () => setIsOpen(false);

const { member, hasMemberFailed, isFetchingMember, error } = useFetchMembers();

const { id, name, created } = member;

if (isFetchingMember) {
return <div>Loading...</div>;
}

if (error) {
return (
<Toast
isOpen={hasMemberFailed}
onClose={handleClose}
variant="danger"
title="We are unable to fetch your member, please try again."
description={(error as any)?.response?.data?.error}
/>
);
}

return (
<>
<h3 className="mt-10 text-2xl font-bold">User Profile</h3>
<h4 className="mt-10 max-w-4xl border-t border-primary-900 pt-4 text-xl font-bold">
User Details
</h4>
<section className="mt-8 max-w-4xl rounded-md border-2 border-secondary-500 pl-6">
<div className="mr-4 mt-3 flex justify-end">
<BlueBars onClick={handleToggleBars} />
<div>{isOpen && <CancelAccount close={handleClose} />} </div>
</div>
<div className="flex gap-16 pt-4 pb-8">
<h6 className="font-bold">User Name:</h6>
<span className="ml-3">{name}</span>
</div>
{/* <div className="flex gap-12 pb-8">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email address isn't in the model for members. It can be discussed if it should be added in sprint 4.

<h6 className="font-bold">Email Address:</h6>
<span>test@example.com</span>
</div> */}
<div className="flex gap-24 pb-8">
<h6 className="font-bold">User ID:</h6>
<span className="ml-3">{id}</span>
</div>
<div className="flex gap-24 pb-8">
<h6 className="font-bold">Created:</h6>
<span className="ml-2">{created}</span>
</div>
</section>
</>
);
}
25 changes: 25 additions & 0 deletions web/beacon-app/src/features/members/hooks/useFetchMembers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';

import axiosInstance from '@/application/api/ApiService';
import { RQK } from '@/constants';

import { memberRequest } from '../api/memberListAPI';
import { MemberQuery } from '../types/memberServices';

export function useFetchMembers(): MemberQuery {
const query = useQuery([RQK.MEMBER_LIST], memberRequest(axiosInstance), {
refetchOnWindowFocus: false,
refetchOnMount: true,
// set stale time to 15 minutes
staleTime: 1000 * 60 * 15,
});

return {
getMembers: query.refetch,
hasMemberFailed: query.isError,
isFetchingMember: query.isLoading,
member: query.data,
wasMemberFetched: query.isSuccess,
error: query.error,
};
}
20 changes: 20 additions & 0 deletions web/beacon-app/src/features/members/types/memberServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface MembersResponse {
member: MemberResponse[];
prev_page_token: string;
next_page_token: string;
}

export interface MemberResponse {
id: string;
name: string;
role: string;
}

export interface MemberQuery {
getMembers(): void;
member: any;
hasMemberFailed: boolean;
wasMemberFetched: boolean;
isFetchingMember: boolean;
error: any;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { vi } from 'vitest';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be great to add a test case where the id is not set


import { orgRequest } from '../orgDetailApi';

describe('Organization', () => {
describe('Organization Detail', () => {
it('returns org details with a given id', async () => {
const mockOrgResponse = {
PromiseRejectionEvent: [
{
id: '1',
name: 'test',
domain: 'test',
created: '02.10.2023',
modified: '02.10.2023',
},
],
};

const requestSpy = vi.fn().mockReturnValueOnce({
status: 200,
data: mockOrgResponse,
statusText: 'OK',
});
const request = orgRequest(requestSpy);
const response = await request('1');
expect(response).toBe(mockOrgResponse);
expect(requestSpy).toHaveBeenCalledTimes(1);
});
});
});
15 changes: 15 additions & 0 deletions web/beacon-app/src/features/organizations/api/orgDetailApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiAdapters } from '@/application/api/ApiAdapters';
import { getValidApiResponse, Request } from '@/application/api/ApiService';
import { APP_ROUTE } from '@/constants';

import { OrgResponse } from '../types/organizationService';

export function orgRequest(request: Request): ApiAdapters['orgDetail'] {
return async (orgID: string) => {
const response = (await request(`${APP_ROUTE.ORG_DETAIL}/${orgID}`, {
method: 'GET',
})) as any;

return getValidApiResponse<OrgResponse>(response);
};
}