Fakultas Ilmu Komputer UI

Commit 8facf5b7 authored by Nabilah Adani's avatar Nabilah Adani
Browse files

Merge branch 'PBI-14-Log-Aktivitas' into 'staging'

Pbi 14 tampilan baru log aktivitas

See merge request !49
parents 3310736c 4f4f3ff3
Pipeline #81677 passed with stages
in 9 minutes and 28 seconds
......@@ -25,6 +25,10 @@ const navigators = [
name: 'Manajemen Akun',
to: '/account-management',
},
{
name: 'Log Aktivitas',
to: '/activity-log',
},
];
const Layout = (props: LayoutProps) => {
......
......@@ -6,6 +6,7 @@ import { Home, AccountManagement, Login, CaseRecapitulation, PositiveCaseInput }
import { LocalStorage } from 'services';
import { DEFAULT_SECRET_KEY } from 'constant';
import Layout from './components/Layout';
import ActivityLog from 'scenes/ActivityLog';
export default function Routes() {
const { user, setUser, setToken, token, services, alert } = useContext(
......@@ -78,6 +79,11 @@ export default function Routes() {
path="/positive-case-input"
component={PositiveCaseInput}
/>
<Route
exact
path="/activity-log"
component={ActivityLog}
/>
</Layout>
</>
) : (
......
......@@ -9,7 +9,6 @@ import Button from 'components/Button';
import Loading from 'components/Loading';
import Icon from 'components/Icon';
type ValueType = string | number;
interface TableProps {
......@@ -17,10 +16,14 @@ interface TableProps {
header: Array<string>;
data?: Array<Array<ValueType>>;
setData?: any;
onChange?: (value: string, pageNumber: number) => Promise<Array<Array<ValueType>>>;
onChange?: (
value: string,
pageNumber: number
) => Promise<Array<Array<ValueType>>>;
searchPlaceholder?: string;
maximumData?: number;
rowOnClick?: (row: Array<ValueType>) => void
rowOnClick?: (row: Array<ValueType>) => void;
itemPerPage?: number;
}
const Click = styled.div`
......@@ -68,7 +71,7 @@ const TableHeader = styled.th`
const LoadingContainer = styled.tr`
height: 400px;
`
`;
const DEFAULT_THEME = {
colors: {
......@@ -91,6 +94,7 @@ export default function Table({
onChange = async () => new Promise<Array<Array<ValueType>>>(() => []),
maximumData = -1,
rowOnClick,
itemPerPage,
}: TableProps) {
const { colors } = useContext(ThemeContext) || DEFAULT_THEME;
const [previousDataTotal, setPreviousDataTotal] = useState(0);
......@@ -116,16 +120,24 @@ export default function Table({
return;
setIsLoading(true);
const newData: Array<Array<ValueType>> = await onChange(
searchValue,
newPageNumber
);
setIsLoading(false);
await onChange(searchValue, newPageNumber).then((newData) => {
setIsLoading(false);
if (newData.length === 0 || newData[0].length === 0) return;
setPageNumber(newPageNumber);
setPreviousDataTotal(previousDataTotal + (sign === 1? data.length: -newData.length));
setData([...newData]);
if (newData.length === 0 || newData[0].length === 0) return;
setPageNumber(newPageNumber);
setPreviousDataTotal(
previousDataTotal + (sign === 1 ? data.length : -newData.length)
);
if (
action === PageAction.Prev &&
previousDataTotal + data.length === maximumData
) {
setPreviousDataTotal(
previousDataTotal - (itemPerPage ? itemPerPage : 10)
);
}
setData([...newData]);
});
};
const [couldGoBack, couldGoNext] = [
......@@ -136,7 +148,7 @@ export default function Table({
useEffect(() => {
setData(data);
}, [JSON.stringify(data)]);
useEffect(() => {
const initializeData = async () => {
setIsLoading(true);
......@@ -149,7 +161,7 @@ export default function Table({
initializeData();
}, []);
if (data[0] && (header.length > data[0].length)) {
if (data[0] && header.length > data[0].length) {
throw new Error('The shape of Header and data are not consistent');
}
......@@ -171,14 +183,14 @@ export default function Table({
<Button
data-test-id="search-button"
onClick={async () => {
const newPageNumber = 1;
const newPageNumber = pageNumber;
setIsLoading(true);
const newData = await onChange(searchValue, newPageNumber);
setIsLoading(false);
if (newData.length === 0 || newData[0].length === 0) return;
setPageNumber(newPageNumber);
setPreviousDataTotal(0);
setData([...newData]);
}}
>
......@@ -218,7 +230,7 @@ export default function Table({
})}
</tr>
</thead>
{isLoading? (
{isLoading ? (
<tbody>
<LoadingContainer>
<Loading isLoading={isLoading} shouldSetBlackTheme={true} />
......@@ -235,20 +247,22 @@ export default function Table({
index % 2 === 0 ? 'transparent' : colors.almostWhite
}
>
{row.filter((_, index) => index < header.length).map((value: ValueType, index: number) => {
return (
<td key={index}>
<Text
width="100%"
type={Text.StyleType.Small}
align="center"
>
{value.toString()}
</Text>
</td>
);
})}
{rowOnClick? (
{row
.filter((_, index) => index < header.length)
.map((value: ValueType, index: number) => {
return (
<td key={index}>
<Text
width="100%"
type={Text.StyleType.Small}
align="center"
>
{value.toString()}
</Text>
</td>
);
})}
{rowOnClick ? (
<td>
<Box
height="100%"
......@@ -269,34 +283,33 @@ export default function Table({
crossAxis="center"
background={colors.green}
>
<Icon src="/assets/icons/zoom.svg" height="50%" cursor="pointer" />
<Icon
src="/assets/icons/zoom.svg"
height="50%"
cursor="pointer"
/>
</Box>
</Click>
</Box>
</td>
) : <></>}
) : (
<></>
)}
</Row>
);
})}
</tbody>
)}
</StyledTable>
{!isLoading && data.length === 0
? (
<Box
width="100%"
height="300px"
mainAxis='center'
crossAxis='center'
>
<Text
type={Text.StyleType.Medium}
color={colors.mediumGray}
>
Tidak ada data
</Text>
</Box>
) : <></>}
{!isLoading && data.length === 0 ? (
<Box width="100%" height="300px" mainAxis="center" crossAxis="center">
<Text type={Text.StyleType.Medium} color={colors.mediumGray}>
Tidak ada data
</Text>
</Box>
) : (
<></>
)}
<Gap gap={20} axis={Gap.Axis.Vertical} />
<Box width="100%" mainAxis="flex-end">
<Box crossAxis="center">
......@@ -310,24 +323,35 @@ export default function Table({
data-test-id="prev-button"
id="prev-button"
cursor="pointer"
onClick={couldGoBack ? () => changePage(PageAction.Prev) : () => {}}
onClick={
couldGoBack && searchValue === ''
? () => changePage(PageAction.Prev)
: () => {}
}
src="/assets/icons/right-paging.svg"
origin="50% 50%"
height="16px"
opacity={couldGoBack ? 1 : 0.5}
opacity={couldGoBack && searchValue === '' ? 1 : 0.5}
transform="rotateZ(180deg)"
/>
<Gap gap={12} axis={Gap.Axis.Horizontal} />
<Text id='page-number' type={Text.StyleType.Small}>{`${pageNumber}`}</Text>
<Text
id="page-number"
type={Text.StyleType.Small}
>{`${pageNumber}`}</Text>
<Gap gap={12} axis={Gap.Axis.Horizontal} />
<Icon
data-test-id="next-button"
id="next-button"
cursor="pointer"
onClick={couldGoNext ? () => changePage(PageAction.Next) : () => {}}
onClick={
couldGoNext && searchValue === ''
? () => changePage(PageAction.Next)
: () => {}
}
src="/assets/icons/right-paging.svg"
height="16px"
opacity={couldGoNext ? 1 : 0.5}
opacity={couldGoNext && searchValue === '' ? 1 : 0.5}
/>
</Box>
</Box>
......
import React from 'react';
import axios from 'axios';
import renderer, { act } from 'react-test-renderer';
import ActivityLog from '.';
import renderer from 'react-test-renderer';
import ActivityList from '.';
import { useMainService } from 'services';
import { AppContext } from 'contexts';
import { mount } from 'enzyme';
const dummyLog = {
status: 200,
data: {
username: "Test",
is_admin: false,
previous: true,
next: true,
count: 1,
results: [
{
action_type: "Delete",
model_name: "Account",
object_id: "12346",
recorded_at: "2020-05-31T23:03:35.854615+07:00",
},
]
}
}
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
const testProps = {
......@@ -14,9 +33,9 @@ const testProps = {
},
};
it('renders correctly', () => {
it('ActivityList renders correctly', () => {
const instance = renderer.create(
<ActivityLog />
<ActivityList />
);
expect(instance).toBeTruthy();
......@@ -45,13 +64,13 @@ describe('load logs and generate log messages correctly', () => {
}
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -75,13 +94,13 @@ describe('load logs and generate log messages correctly', () => {
}
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -102,13 +121,13 @@ describe('load logs and generate log messages correctly', () => {
],
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -131,13 +150,13 @@ describe('load logs and generate log messages correctly', () => {
]
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -158,13 +177,13 @@ describe('load logs and generate log messages correctly', () => {
]
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -187,13 +206,13 @@ describe('load logs and generate log messages correctly', () => {
]
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -214,13 +233,13 @@ describe('load logs and generate log messages correctly', () => {
]
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
......@@ -243,96 +262,55 @@ describe('load logs and generate log messages correctly', () => {
]
}
});
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
});
});
it('fetch new logs and change page number when press next or previous button', () => {
mockedAxios.request.mockResolvedValue({
status: 200,
data: {
username: "Test",
is_admin: false,
previous: true,
next: true,
count: 1,
results: [
{
action_type: "Delete",
model_name: "Account",
object_id: "1234",
recorded_at: "2020-05-31T23:03:35.854615+07:00",
},
]
}
});
mockedAxios.request.mockResolvedValue(dummyLog);
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
const prevButton = instance.find('#prev-button');
const nextButton = instance.find('#next-button');
let pageNumber = instance.find('Text').findWhere(elem => elem.prop('id') === 'page-number');
expect(pageNumber.text()).toBe('1');
nextButton.at(0).simulate('click');
expect(mockedAxios.request).toBeCalled();
prevButton.at(0).simulate('click');
pageNumber = instance.find('Text').findWhere(elem => elem.prop('id') === 'page-number');
expect(pageNumber.text()).toBe('1');
expect(mockedAxios.request).toBeCalled();
})
it('Sort and filter are displayed and clickable', () => {
mockedAxios.request.mockResolvedValue({
status: 200,
data: {
username: "Test",
is_admin: false,
previous: true,
next: true,
count: 1,
results: [
{
action_type: "Delete",
model_name: "Account",
object_id: "1234",
recorded_at: "2020-05-31T23:03:35.854615+07:00",
},
]
}
});
it('filter are displayed and clickable', () => {
mockedAxios.request.mockResolvedValue(dummyLog);
const instance = mount(
<AppContext.Provider value={testProps}>
<ActivityLog />
<ActivityList />
</AppContext.Provider>
);
expect(mockedAxios.request).toBeCalled();
let categoryButton = instance.find('[data-test-id="button-Tabel"]');
categoryButton.at(0).simulate('click');
let sortByDate = instance.find("#sortbydate");
let sortByObj = instance.find("#sortbyobj");
let filter = instance.find("#filter");
sortByDate.simulate("click");
sortByObj.simulate("click");
filter.simulate("click");
let table = instance.find("Table");
expect(table).toBeTruthy();
let list = instance.find("#list-activity");
expect(list).toBeTruthy();
});
\ No newline at end of file
import React, { useContext, useEffect, useState } from 'react';
import styled, { ThemeContext } from 'styled-components';
import { Box, Text, Gap, Icon, Field, Button, Checkbox } from 'components';
import { AppContext } from 'contexts';
import Loading from 'components/Loading';
import { DateProps } from 'contexts/AppContext/types';
import { Log } from 'scenes/ActivityLog/types/types';
import {
generateLogDetail,
generateLogMessage,
} from 'scenes/ActivityLog/utilities/utils';
import { DEFAULT_THEME } from 'scenes/ActivityLog';
export default function ActivityList() {
const { colors } = useContext(ThemeContext) || DEFAULT_THEME;
const { services } = useContext(AppContext);
const [logList, setLogList] = useState<Log[]>([]);
const [page, setPage] = useState(1);
const [next, setNext] = useState(false);
const [prev, setPrev] = useState(false);
const [totalLog, setTotalLog] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const verticalLineHeight = (logList.length - 1) * 52;
const [roleFilterQuery, setRoleFilterQuery] = useState<String>('Semua Peran');
const [ignoreDateFilterQuery, setIgnoredateFilterQuery] = useState<boolean>(
true
);
const [dateFilterQuery, setDateFilterQuery] = useState<DateProps>({
start_date: new Date(),
end_date: new Date(),
});
const VerticalLine = styled.div`
border-left: 2px solid ${colors.mediumGray};
height: ${verticalLineHeight}px;
position: absolute;
left: 12px;
top: 5px;
`;
const Circle = styled.div`
left: 12px;
width: 24px;
height: 24px;
border-radius: 12px;
background: ${colors.mediumGray};
margin-right: 20px;