37 Commits

Author SHA1 Message Date
SA K
baa433ec46 Merge pull request #418 from sakang07/develop
Apply mobile responsive code and refactoring
2023-05-23 17:44:40 +09:00
SA K
3a93b1421b [FE-header] Show widget menu in mobile view 2023-05-23 17:38:25 +09:00
SA K
2d4d3bd1dc [FE-header] Show widget menu in mobile view 2023-05-23 17:33:13 +09:00
SA K
d2f1066336 [FE] Merge extra code 2023-05-23 17:17:29 +09:00
SA K
e0d6a805c6 Merge remote-tracking branch 'fork/develop' into develop 2023-05-23 17:08:56 +09:00
SA K
44894701a8 [FE-widget] Fix column chart and dataset button in widget creation 2023-05-23 17:07:35 +09:00
SA K
1b769d2f16 [FE-chore] Fix header name 2023-05-23 17:06:45 +09:00
SA K
79de46207e [FE-data] Fix public footer styles and Refactor components 2023-05-23 17:04:02 +09:00
SA K
d11ab4ad03 Merge remote-tracking branch 'fork/develop' into develop 2023-05-23 16:44:34 +09:00
SA K
b786883310 [FE-share] Add a mobile responsive view 2023-05-23 16:44:20 +09:00
SA K
adb417ea5d [FE-style] Add ellipsis when text length is long 2023-05-23 16:42:39 +09:00
SA K
9fd1fd7a2e [FE] Fix alert window 2023-05-23 16:42:32 +09:00
SA K
348cc486e5 Merge remote-tracking branch 'upstream/develop' into develop 2023-05-23 16:27:57 +09:00
SA K
913a624631 [FE] Fix styles about header and board 2023-05-23 16:19:18 +09:00
SA K
e8d7c80fc3 [FE-dashboard] Fixed an issue that does not change size until a refresh when resizing the table widget. 2023-05-23 16:15:10 +09:00
SA K
87f90ac52a [FE-widget] Add a horizontal scroll when viewport width is small 2023-05-23 16:14:39 +09:00
SA K
0dafb5b639 [FE] Fix alert styles 2023-05-23 16:14:34 +09:00
SA K
28d40b2a65 [FE-data] Fix data grid 2023-05-23 16:14:09 +09:00
SA K
bf87f5babc [FE-data] Add a modal window 2023-05-23 16:08:50 +09:00
손승우
488c4180ed update: change typeorm-seeder => typeorm-extension 2023-05-23 16:02:28 +09:00
SA K
8a6f0ef3b7 [FE-data] Refactor card components 2023-05-22 23:45:25 +09:00
SA K
b5b22fda13 [FE-data] Fix styles 2023-05-22 23:45:19 +09:00
SA K
b7db3d799a [FE-layout] Modify the responsive views more smoothly 2023-05-22 23:45:13 +09:00
SA K
6ae443fe17 [FE-data] Refactor card components 2023-05-22 23:45:08 +09:00
SA K
1ad5516366 [FE] Fix axios setting 2023-05-22 23:44:59 +09:00
SA K
1fdb95f3f6 [FE-data] Remove selected styles in data page 2023-05-22 23:44:55 +09:00
SA K
5072aed994 [FE-data] Remove selected styles in data page 2023-05-22 23:44:52 +09:00
SA K
fb755b6a2e [FE-data] Add a preview window of data 2023-05-22 23:44:46 +09:00
SA K
e9190e447a [FE-login] fix desktop view styles 2023-05-22 23:44:41 +09:00
SA K
16848c0c83 [FE-data] Add a preview window of data: step 1 2023-05-22 23:44:37 +09:00
SA K
952e6bdc47 [FE-login] Add mobile view 2023-05-22 23:44:31 +09:00
SA K
5b59d42fc5 [FE-dashboard] Fix datePicker in mobile view 2023-05-22 23:44:27 +09:00
SA K
f7170f1d75 [FE-layout] Add mobile layout 2023-05-22 23:44:23 +09:00
SA K
1b8600ee27 [FE-layout] Add mobile layout: step 3 2023-05-22 23:44:04 +09:00
SA K
70afd666fe [FE-layout] Add mobile layout: step 2 2023-05-22 23:43:53 +09:00
SA K
4133c21a30 [FE-layout] Add mobile layout: step 1 2023-05-22 23:43:45 +09:00
SA K
61774665b7 [FE-data/widget] Fix layout 2023-05-22 23:43:35 +09:00
68 changed files with 2235 additions and 1554 deletions

View File

@@ -20,8 +20,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"deploy": "serverless deploy --stage prod",
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
"seed:run": "ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
"seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-extension/dist/cli/index.js seed",
"deploy:dev": "serverless deploy --stage dev",
"deploy:prod": "serverless deploy --stage prod"
},
@@ -78,8 +77,8 @@
"tedious": "^15.1.0",
"ts-jenum": "^2.2.2",
"typeorm": "^0.3.9",
"typeorm-extension": "^2.8.0",
"typeorm-naming-strategies": "^4.1.0",
"typeorm-seeding": "^1.6.1",
"uuid": "^9.0.0",
"winston": "^3.8.2"
},

View File

@@ -0,0 +1,15 @@
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';
config();
const configService = new ConfigService();
export default new DataSource({
type: 'sqlite',
database: 'sqlite.db',
synchronize: true,
entities: ['src/**/*.entity{.ts,.js}', 'src/**/*.enum{.ts,.js}'],
logging: true,
});

View File

@@ -1,5 +1,5 @@
import { Connection } from "typeorm";
import { Seeder,Factory } from "typeorm-seeding"
import { DataSource } from "typeorm";
import { SeederFactoryManager, Seeder } from "typeorm-extension"
import { Component } from "../../component/entities/component.entity";
import { DatabaseType } from "../../database/entities/database_type.entity";
import { Template } from "../../template/entities/template.entity";
@@ -7,10 +7,13 @@ import { TemplateItem } from "../../template/entities/template-item.entity";
import { User } from "../../user/entities/user.entity"
import { YesNo } from '../../common/enum/yn.enum';
import { setSeederFactory } from 'typeorm-extension';
export class CreateInitialData implements Seeder {
public async run(factory:Factory , connection:Connection) : Promise<any>{
await connection
export default class CreateInitialData implements Seeder {
public async run ( dataSource: DataSource) : Promise<any>{
await dataSource
.createQueryBuilder()
.insert()
.into(Component)
@@ -696,8 +699,7 @@ export class CreateInitialData implements Seeder {
)
.execute()
await connection
await dataSource
.createQueryBuilder()
.insert()
.into(DatabaseType)
@@ -836,7 +838,7 @@ export class CreateInitialData implements Seeder {
.execute();
await connection
await dataSource
.createQueryBuilder()
.insert()
.into(Template)
@@ -926,7 +928,7 @@ export class CreateInitialData implements Seeder {
await connection
await dataSource
.createQueryBuilder()
.insert()
.into(TemplateItem)
@@ -1486,7 +1488,7 @@ export class CreateInitialData implements Seeder {
])
.execute();
await connection
await dataSource
.createQueryBuilder()
.insert()
.into(User)
@@ -1499,4 +1501,5 @@ export class CreateInitialData implements Seeder {
}
])
.execute();
}}
}}

View File

@@ -28,6 +28,7 @@
"echarts": "^5.3.3",
"echarts-for-react": "^3.0.2",
"echarts-gl": "^2.0.9",
"lodash": "^4.17.21",
"mathjs": "^11.3.3",
"react": "^18.2.0",
"react-ace": "^10.1.0",

View File

@@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 3.516v1.509c0 1.39-3.134 2.516-7 2.516S2 6.415 2 5.025v-1.51C2 2.127 5.134 1 9 1s7 1.126 7 2.516zM14.287 7.75c.622-.232 1.247-.531 1.713-.899v3.204c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516V6.852c.467.368 1.063.667 1.714.9 1.4.502 3.27.795 5.286.795 2.016 0 3.884-.293 5.287-.796zM3.714 12.783c1.4.503 3.27.795 5.286.795 2.016 0 3.884-.292 5.287-.795.622-.233 1.247-.532 1.713-.9v2.701c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516v-2.7c.467.367 1.063.666 1.714.899z" fill="#0F5AB3"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="inherit" xmlns="http://www.w3.org/2000/svg">
<path d="M16 3.516v1.509c0 1.39-3.134 2.516-7 2.516S2 6.415 2 5.025v-1.51C2 2.127 5.134 1 9 1s7 1.126 7 2.516zM14.287 7.75c.622-.232 1.247-.531 1.713-.899v3.204c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516V6.852c.467.368 1.063.667 1.714.9 1.4.502 3.27.795 5.286.795 2.016 0 3.884-.293 5.287-.796zM3.714 12.783c1.4.503 3.27.795 5.286.795 2.016 0 3.884-.292 5.287-.795.622-.233 1.247-.532 1.713-.9v2.701c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516v-2.7c.467.367 1.063.666 1.714.899z" />
</svg>

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -0,0 +1,11 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#wy8iit3taa)" fill="#fff">
<path d="M4.94 17.32a3.998 3.998 0 0 1-2.83-6.83l1.41-1.41a.996.996 0 1 1 1.41 1.41L3.52 11.9c-.78.78-.78 2.05 0 2.83.78.78 2.05.78 2.83 0l2.83-2.83c.38-.38.58-.88.58-1.41 0-.53-.21-1.04-.58-1.41a.996.996 0 1 1 1.41-1.41c.75.75 1.17 1.76 1.17 2.83s-.41 2.08-1.17 2.83l-2.83 2.83c-.78.78-1.8 1.17-2.83 1.17l.01-.01z"/>
<path d="M7.77 11.5c-.26 0-.51-.1-.71-.29a3.987 3.987 0 0 1-1.17-2.83c0-1.07.41-2.08 1.17-2.83l2.83-2.83c.75-.75 1.76-1.17 2.83-1.17s2.08.41 2.83 1.17c.75.75 1.17 1.76 1.17 2.83s-.42 2.08-1.17 2.83l-1.41 1.41a.996.996 0 1 1-1.41-1.41l1.41-1.41c.38-.38.58-.88.58-1.41 0-.53-.21-1.04-.58-1.41-.75-.75-2.08-.75-2.83 0L8.48 6.98c-.38.38-.58.88-.58 1.41 0 .53.21 1.04.58 1.41a.996.996 0 0 1-.71 1.7z"/>
</g>
<defs>
<clipPath id="wy8iit3taa">
<path fill="#fff" d="M0 0h18v18H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

View File

@@ -0,0 +1,137 @@
import React from 'react';
import { Box, Hidden, ListItem, ListItemIcon, Stack, useMediaQuery, useTheme } from '@mui/material';
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
import DeleteButton from '@/components/button/DeleteButton';
import ModifyButton from '@/components/button/ModifyButton';
import { styled } from '@mui/system';
import { dateData } from '@/utils/util';
interface BoardItemDataProps {
id: string;
title: string;
updatedAt: string;
componentType?: string;
icon?: string;
}
interface BoardItemProps {
data: BoardItemDataProps;
handleDeleteSelect: (id, title) => void;
}
const tableBorder = '1px solid #DADDDD';
const MobileTitleSpan = styled('span')({
display: 'block',
flexGrow: 0,
width: '100%',
fontSize: '14px',
fontWeight: '600',
lineHeight: '1.43',
color: '#333333',
});
const TitleSpan = styled('span')({
display: 'block',
flexGrow: 0,
width: '100%',
height: '14px',
fontSize: '14px',
fontWeight: '600',
lineHeight: '1.14',
color: '#333333',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
});
interface SubTitleSpanProps {
children: React.ReactNode;
matches?: boolean;
}
const SubTitleSpan = styled('span')<SubTitleSpanProps>(matches => ({
display: 'flex',
height: '14px',
justifyContent: 'space-between',
fontSize: matches ? '14px' : '10px',
fontWeight: '500',
lineHeight: '1.14',
color: '#333333',
}));
const IconRowHeader = ({ icon }) => {
return (
<ListItemIcon
sx={{
minWidth: '24px',
mr: '18px',
}}
>
<Box
component="img"
src={`static/images/${icon}`}
sx={{ width: 'auto', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
/>
</ListItemIcon>
);
};
function BoardItem(props: BoardItemProps) {
const { data, handleDeleteSelect } = props;
const { id, title, componentType, icon, updatedAt } = data;
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const navigate = useNavigate();
const location = useLocation();
const pathname = location.pathname;
return (
<ListItem
disablePadding
sx={{
py: { xs: '20px', sm: '7px' },
px: { xs: '16px', sm: '20px' },
pr: { sm: '28px' },
borderBottom: tableBorder,
'&:last-of-type': { borderBottom: 0 },
}}
component={RouterLink}
to={`${id}`}
state={{ from: pathname }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
<Stack direction="row" alignItems="center" sx={{ width: '100%', maxWidth: `calc(100% - ${matches ? 300 : 110}px)` }}>
{matches && componentType && <IconRowHeader icon={icon} />}
{matches ? <TitleSpan>{title}</TitleSpan> : <MobileTitleSpan>{title}</MobileTitleSpan>}
</Stack>
<Stack alignItems="center" direction="row">
<SubTitleSpan matches={matches}>{dateData(updatedAt)}</SubTitleSpan>
<Hidden smDown>
<Stack direction="row" gap="18px" ml="48px">
<ModifyButton
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
navigate(`modify?id=${id}&title=${title}`, { state: { from: pathname } });
}}
/>
<DeleteButton
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDeleteSelect(id, title);
}}
/>
</Stack>
</Hidden>
</Stack>
</Stack>
</ListItem>
);
}
export default BoardItem;

View File

@@ -1,7 +1,25 @@
import React, { useEffect, useState } from 'react';
import { Box, List, Pagination, Stack, useMediaQuery, useTheme } from '@mui/material';
import BoardListItem from './BoardListItem';
import BoardItem from './BoardItem';
import { styled } from '@mui/system';
import { MAX_WIDTH } from '@/constant';
interface GTSpanProps {
children: React.ReactNode;
matches?: boolean;
isWidget?: boolean;
}
// TODO: 오류 수정
const GTSpan = styled('span')<GTSpanProps>(props => ({
marginLeft: props.matches && props.isWidget && '50px',
fontSize: props.matches ? '13px' : '10px',
fontWeight: '500',
lineHeight: '1.23',
color: '#767676',
}));
const tableBorder = '1px solid #DADDDD';
function BoardList(props) {
const { postList, handleDeleteSelect } = props;
@@ -12,32 +30,18 @@ function BoardList(props) {
setTotalCount(Math.ceil(postList.length / 10));
}, [postList]);
const GTSpan = styled('span')({
marginLeft: postList[0]?.componentType && '50px',
fontFamily: 'Pretendard',
fontSize: '13px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.23',
letterSpacing: 'normal',
textAlign: 'left',
color: '#767676',
});
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const tableBorder = '1px solid #DADDDD';
const handlePageChange = (e, p) => {
setPage(p);
};
const generateBoardItem = () => {
return postList.map((item, i) => {
return postList.map((item, index) => {
const currPage = (page - 1) * 10;
if (i >= currPage && i < currPage + 10) {
return <BoardListItem postItem={item} key={item.id} handleDeleteSelect={handleDeleteSelect} />;
if (index >= currPage && index < currPage + 10) {
return <BoardItem data={item} key={item.id} handleDeleteSelect={handleDeleteSelect} />;
} else {
return null;
}
@@ -45,16 +49,24 @@ function BoardList(props) {
};
return (
<Box>
<Box sx={{ maxWidth: MAX_WIDTH, width: '100%', mx: 'auto' }}>
<Stack
flexDirection="row"
justifyContent="space-between"
sx={{ paddingLeft: '20px', paddingRight: '206px', marginBottom: '11px', marginTop: '36px' }}
sx={{
width: '100%',
paddingLeft: '20px',
paddingRight: { xs: '60px', sm: '216px' },
marginBottom: '11px',
marginTop: { xs: '21px', sm: '36px' },
}}
>
<GTSpan></GTSpan>
{matches && <GTSpan></GTSpan>}
<GTSpan isWidget={Boolean(postList?.[0]?.componentType)} matches={matches}>
</GTSpan>
<GTSpan matches={matches}></GTSpan>
</Stack>
<List sx={{ m: 'auto', border: tableBorder, borderRadius: 2, backgroundColor: '#fff' }} disablePadding>
<List sx={{ width: '100%', m: 'auto', border: tableBorder, borderRadius: 2, backgroundColor: '#fff' }} disablePadding>
{generateBoardItem()}
</List>
<Stack alignItems="center" sx={{ marginTop: '47px' }}>

View File

@@ -1,113 +0,0 @@
import React from 'react';
import { Avatar, ListItem, ListItemIcon, Stack } from '@mui/material';
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
import DeleteButton from '@/components/button/DeleteButton';
import ModifyButton from '@/components/button/ModifyButton';
import { styled } from '@mui/system';
import { dateData } from '@/utils/util';
const tableBorder = '1px solid #DADDDD';
function BoardListItem(props) {
const { postItem, handleDeleteSelect } = props;
const navigate = useNavigate();
const location = useLocation();
const pathname = location.pathname;
const TitleSpan = styled('span')({
display: 'flex',
height: '14px',
justifyContent: 'space-between',
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: '600',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333333',
});
const SubTitleSpan = styled('span')({
display: 'flex',
height: '14px',
justifyContent: 'space-between',
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333333',
});
return (
<ListItem
key={postItem.id}
disablePadding
sx={{
borderBottom: tableBorder,
'&:last-of-type': { borderBottom: 0 },
height: '56px',
paddingRight: 0,
}}
component={RouterLink}
to={`${postItem.id}`}
state={{ from: pathname }}
>
{postItem.componentType ? (
<ListItemIcon
sx={{
minWidth: '24px',
ml: '20px',
mr: '-2px',
}}
>
<Avatar
src={`static/images/${postItem.icon}`}
sx={{ width: 'auto', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
/>
</ListItemIcon>
) : (
''
)}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ x: 0, paddingLeft: '20px', width: '100%' }}
>
<TitleSpan>{postItem.title}</TitleSpan>
<Stack alignItems="center" direction="row" sx={{ paddingRight: '36px' }}>
<SubTitleSpan>{dateData(postItem.updatedAt)}</SubTitleSpan>
<span style={{ width: '56px' }} />
<ModifyButton
size="medium"
sx={{ padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
navigate(`modify?id=${postItem.id}&title=${postItem.title}`, { state: { from: pathname } });
}}
/>
<span style={{ width: '36px' }} />
<DeleteButton
size="medium"
sx={{ padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDeleteSelect(postItem);
}}
/>
</Stack>
</Stack>
</ListItem>
);
}
export default BoardListItem;

View File

@@ -1,18 +0,0 @@
import { Link, Typography } from '@mui/material';
import React from 'react';
const Copyright = (props: any) => {
return (
<Typography color="text.secondary" align="center" {...props}>
<Link
color="inherit"
href="https://vanillabrain.com/"
sx={{ fontSize: '13px', color: '#767676', fontWeight: 'bold', textDecoration: 'none' }}
>
VanillaBrain Inc.
</Link>
</Typography>
);
};
export default Copyright;

View File

@@ -1,6 +1,29 @@
import React from 'react';
import { Box, CardContent, Typography } from '@mui/material';
import { CardWrapper } from '@/components/list/CardListWrapper';
import { Box, Card, CardContent, Typography } from '@mui/material';
const CardWrapper = ({ children, selected, onClick, sx = null }) => {
return (
<Card
sx={{
padding: '20px 8px 20px 21px',
borderRadius: '8px',
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
backgroundColor: selected ? '#edf8ff' : '#fff',
cursor: 'pointer',
position: 'relative',
alignItems: 'center',
'&:hover': {
backgroundColor: '#ebfbff',
},
...sx,
}}
onClick={onClick}
>
{children}
</Card>
);
};
function ImgCardList(props) {
const { data, selectedType, handleTypeClick } = props;

View File

@@ -1,6 +1,29 @@
import React from 'react';
import { Box, CardContent, Typography } from '@mui/material';
import { CardWrapper } from '@/components/list/CardListWrapper';
import { Box, Card, CardContent, Typography } from '@mui/material';
const CardWrapper = ({ children, selected, onClick, sx = null }) => {
return (
<Card
sx={{
padding: '20px 8px 20px 21px',
borderRadius: '8px',
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
backgroundColor: selected ? '#edf8ff' : '#fff',
cursor: 'pointer',
position: 'relative',
alignItems: 'center',
'&:hover': {
backgroundColor: '#ebfbff',
},
...sx,
}}
onClick={onClick}
>
{children}
</Card>
);
};
function LargeImgCardList(props) {
const { data, selectedType, setSelectedType } = props;

View File

@@ -0,0 +1,70 @@
import { Box, IconButton, Modal, Paper, Stack, Typography } from '@mui/material';
import { ReactComponent as CloseIcon } from '@/assets/images/icon/ic-xmark.svg';
import React from 'react';
import { MAX_WIDTH } from '@/constant';
interface ModalPopupProps {
open: boolean;
handleClose: () => void;
title: string | React.ReactNode;
children: React.ReactNode;
}
const ModalPopup = (props: ModalPopupProps) => {
const { open, handleClose, title, children } = props;
return (
<Modal
open={open}
onClose={handleClose}
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
BackdropProps={{
sx: {
boxShadow: '0 4px 4px 0 rgba(0, 0, 0, 0.25)',
backgroundColor: 'rgba(122, 130, 144, 0.45)',
},
}}
>
<Paper
sx={{
display: 'flex',
flexDirection: 'column',
width: { xs: '90%', sm: '80%' },
maxWidth: MAX_WIDTH,
height: '70%',
maxHeight: '754px',
borderRadius: '8px',
boxShadow: '5px 5px 8px 0 rgba(0, 28, 71, 0.15)',
border: 'solid 1px #ddd',
p: '10px',
pt: 0,
backgroundColor: '#fff',
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center" m="20px" mr="10px">
<Typography sx={{ fontSize: '20px', fontWeight: 600, color: '#141414' }}>{title}</Typography>
<IconButton onClick={handleClose} sx={{ p: '10px' }}>
<CloseIcon width="16" height="16" />
</IconButton>
</Stack>
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
}}
>
{children}
</Box>
</Paper>
</Modal>
);
};
export default ModalPopup;

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { Stack } from '@mui/material';
function PageContainer(props) {
return <Stack sx={{ width: '100%', height: '100%' }}>{props.children}</Stack>;
}
export default PageContainer;

View File

@@ -1,15 +1,26 @@
import React from 'react';
import { Box, Stack, Typography } from '@mui/material';
import { Hidden, Stack, SxProps, Typography } from '@mui/material';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
function PageTitleBox(props) {
const { title, upperTitle, upperTitleLink = '', button, sx = {}, fixed } = props;
interface PageTitleBoxProps {
title: string;
upperTitle?: string;
upperTitleLink?: string;
sx?: SxProps;
button?: React.ReactNode;
fixed?: boolean;
children: React.ReactNode;
}
function PageTitleBox(props: PageTitleBoxProps) {
const { title, upperTitle, upperTitleLink, button, sx, fixed, children } = props;
const navigate = useNavigate();
const defaultBoxSx = {
flex: '1 1 auto',
paddingLeft: '25px',
paddingRight: '25px',
width: '100%',
width: '100vw',
height: '100%',
};
@@ -18,7 +29,7 @@ function PageTitleBox(props) {
Object.assign(defaultBoxSx, sx);
return (
<Box sx={{ width: '100%', height: '100%' }}>
<Stack direction="column" sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
<Stack
sx={{
width: '100%',
@@ -30,8 +41,7 @@ function PageTitleBox(props) {
justifyContent="space-between"
sx={{
width: '100%',
minWidth: '400px',
height: 56,
height: { xs: 40, sm: 56 },
paddingLeft: '24px',
paddingRight: '24px',
borderBottom: '1px solid #e3e7ea',
@@ -39,7 +49,7 @@ function PageTitleBox(props) {
...fixedSx,
}}
>
<Stack direction="row" gap="10px" alignItems="center">
<Stack direction="row" gap={{ xs: '6px', sm: '10px' }} alignItems="center">
{upperTitle && (
<>
<Typography
@@ -49,7 +59,7 @@ function PageTitleBox(props) {
height: '19px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '16px',
fontSize: { xs: '14px', sm: '16px' },
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
@@ -67,7 +77,7 @@ function PageTitleBox(props) {
height: '19px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '16px',
fontSize: 'inherit',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
@@ -94,7 +104,7 @@ function PageTitleBox(props) {
padding: 0,
cursor: 'pointer',
fontFamily: 'Pretendard',
fontSize: '16px',
fontSize: { xs: '14px', sm: '16px' },
fontWeight: '600',
fontStretch: 'normal',
fontStyle: 'normal',
@@ -109,19 +119,19 @@ function PageTitleBox(props) {
{title}
</Typography>
</Stack>
{button}
<Hidden smDown>{button}</Hidden>
</Stack>
</Stack>
<Box sx={{ width: '100%', height: 'calc(100% - 56px)', display: 'flex', justifyContent: 'center' }}>
<Box sx={defaultBoxSx}>{props.children}</Box>
</Box>
</Box>
<Stack
direction="column"
sx={{ justifyContent: 'flex-start', flex: '1 1 auto', width: '100%', height: 'calc(100% - 56px)' }}
>
<Stack direction="column" sx={defaultBoxSx}>
{children}
</Stack>
</Stack>
</Stack>
);
}
PageTitleBox.defaultProps = {
title: '',
upperTitle: '',
};
export default PageTitleBox;

View File

@@ -0,0 +1,228 @@
import React, { useContext, useEffect } from 'react';
import { Avatar, Box, Divider, Stack, SxProps, Typography, useMediaQuery, useTheme } from '@mui/material';
import { MAX_WIDTH } from '@/constant';
import { LayoutContext } from '@/contexts/LayoutContext';
interface PageViewBoxProps {
iconName?: string;
title?: string;
titleElement?: React.ReactNode;
date?: string;
button?: React.ReactNode;
sx?: SxProps;
children?: React.ReactNode;
}
function PageViewBox(props: PageViewBoxProps) {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
return matches ? <DesktopViewBox {...props} /> : <MobileViewBox {...props} />;
}
export default PageViewBox;
const MobileViewBox = props => {
const { iconName, title, titleElement, date, button, sx } = props;
const { changeFooterBg } = useContext(LayoutContext);
useEffect(() => {
changeFooterBg('#f9f9fa');
return () => {
changeFooterBg(null);
};
}, []);
return (
<Box
sx={{
display: 'flex',
flex: '1 1 auto',
width: '100%',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{
width: '100%',
minHeight: '66px',
px: '20px',
backgroundColor: '#ffffff',
...sx,
}}
>
<Stack direction="row" alignItems="center">
{iconName && (
<Avatar
src={`/static/images/${iconName}`}
sx={{
width: '30px',
height: '30px',
marginRight: '12px',
borderRadius: 0,
objectFit: 'contain',
backgroundColor: 'transparent',
}}
/>
)}
<Stack direction="column" gap="4px" sx={{ mt: '18px', mb: '10px' }}>
{titleElement ? (
titleElement
) : (
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: '3',
WebkitBoxOrient: 'vertical',
maxHeight: '60px',
pr: '12px',
fontSize: '16px',
fontWeight: 600,
lineHeight: 1.3,
color: '#333',
textOverflow: 'ellipsis',
overflow: 'hidden',
wordWrap: 'break-word',
}}
>
{title}
</Typography>
)}
{date && (
<Typography sx={{ fontSize: '10px', fontWeight: 500, lineHeight: 1.6, color: '#333' }}>
: {date}
</Typography>
)}
</Stack>{' '}
</Stack>
{button}
</Stack>
<Box
sx={{
width: '100%',
minWidth: '100%',
height: '100%',
flex: '1 1 auto',
backgroundColor: '#f9f9fa',
}}
>
<Divider sx={{ width: '100%', height: '1px' }} />
{props.children}
</Box>
</Box>
);
};
const DesktopViewBox = props => {
const { iconName, title, titleElement, date, button, sx } = props;
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
px: '20px',
}}
>
<Box
sx={{
width: '100%',
maxWidth: MAX_WIDTH,
height: '100%',
borderRadius: '6px',
border: 'solid 1px #ddd',
backgroundColor: '#f9f9fa',
...sx,
}}
>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ width: '100%', height: '57px', px: '20px', backgroundColor: '#ffffff', borderRadius: '6px 6px 0px 0px' }}
>
{/* title */}
<Stack
direction="row"
alignItems="center"
sx={{
width: '100%',
maxWidth: { sm: `calc(100% - ${button ? 390 : 100}px)`, md: `calc(100% - ${button ? 360 : 100}px)` },
}}
>
{iconName && (
<Avatar
src={`/static/images/${iconName}`}
sx={{
width: '30px',
height: '30px',
borderRadius: 0,
objectFit: 'contain',
backgroundColor: 'transparent',
mr: '18px',
}}
/>
)}
{titleElement ? (
titleElement
) : (
<Typography
variant="subtitle1"
component="span"
sx={{
flexGrow: 0,
display: 'block',
width: '100%',
maxWidth: MAX_WIDTH,
height: '16px',
fontWeight: 500,
fontSize: { xs: '16px', sm: '18px' },
lineHeight: 0.89,
letterSpacing: '-0.18px',
color: '#141414',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
>
{title}
</Typography>
)}
</Stack>
{/* date, button */}
<Stack direction="row" justifyContent="flex-end" alignItems="center" flexShrink="0">
<Box
component="span"
sx={{
marginRight: button ? { sm: '26px', md: '36px' } : {},
height: '16px',
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333333',
}}
>
{date}
</Box>
<Stack direction="row" justifyContent="flex-end" alignItems="center">
{button}
</Stack>
</Stack>
</Stack>
<Divider sx={{ width: '100%', height: '1px' }} />
{props.children}
</Box>
</Box>
);
};

View File

@@ -41,7 +41,7 @@ const AlertTemplate = ({ close, message, options }: IProps) => {
return (
<Dialog
open={true}
// onClose={close}
onClose={close}
keepMounted
aria-labelledby="alert-dialog-slide-title"
aria-describedby="alert-dialog-slide-description"
@@ -65,7 +65,8 @@ const AlertTemplate = ({ close, message, options }: IProps) => {
fontSize: '14px',
color: 'black',
paddingTop: hasTitle ? 0 : '20px',
minWidth: 300,
width: '100%',
minWidth: { xs: 0, sm: 300 },
maxWidth: 500,
whiteSpace: 'pre-wrap',
textAlign: 'center',

View File

@@ -97,9 +97,9 @@ export const AddMenuIconButton = ({
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
color="primary"
sx={{ minWidth: 0, padding: 0 }}
sx={{ minWidth: 0, padding: 0, flex: '1 1 auto' }}
>
<Box component="img" src={iconUrl} sx={sizeOption} alt="추가메뉴 활성화" />
<Box component="img" src={iconUrl} sx={sizeOption} alt="추가메뉴" />
</Button>
<Menu id="styled-menu" anchorEl={anchorEl} open={open} onClose={handleClose} sx={{ width: menuWidth }}>
{menuList.map(item => (

View File

@@ -1,26 +1,48 @@
import React from 'react';
import { Box, Button, ClickAwayListener, IconButton, Paper, Popper, Stack, TextField, Typography } from '@mui/material';
import React, { createRef, forwardRef, Ref, useRef } from 'react';
import {
Box,
Button,
ClickAwayListener,
IconButton,
Paper,
Popper,
Stack,
SxProps,
TextField,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { ReactComponent as IconShare } from '@/assets/images/icon/ic-share.svg';
import { ReactComponent as IconToggleOn } from '@/assets/images/icon/toggle-on.svg';
import { ReactComponent as IconToggleOff } from '@/assets/images/icon/toggle-off.svg';
import { ReactComponent as IconLink } from '@/assets/images/icon/ic-link.svg';
import { useAlert } from 'react-alert';
import { ROUTE_URL } from '@/constant';
import DatePicker from '@/components/form/DatePicker';
const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShareLimitDate }) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
const shareLink = `${ROUTE_URL}/share/${shareId ? shareId : ''}`;
interface ShareButtonProps {
handleShareToggle?: () => void;
isShareOn?: boolean;
shareId?: string;
shareLimitDate?: string;
setShareLimitDate?: React.Dispatch<React.SetStateAction<string>>;
}
interface SharePopupProps extends ShareButtonProps {
matches: boolean;
}
const paperSx: SxProps = {
marginTop: '3px',
border: 'solid 1px #ddd',
borderRadius: '6px',
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
};
const ShareOnPopup = forwardRef((props: SharePopupProps, ref: Ref<HTMLDivElement>) => {
const { matches, handleShareToggle, shareLimitDate, shareId } = props;
const alert = useAlert();
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const shareLink = `${window.location.origin}/share/${shareId ? shareId : ''}`;
const handleCopyClick = async () => {
await navigator.clipboard
@@ -34,22 +56,192 @@ const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShar
});
};
return matches ? (
<Paper sx={{ ...paperSx, minWidth: '410px' }} ref={ref}>
<Stack sx={{ width: '100%', padding: '24px' }}>
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}> </Typography>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleShareToggle();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
mb="3px"
>
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}> .</Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOn />
</IconButton>
</Stack>
<Typography>
&nbsp;
<Box component="span" sx={{ color: '#0f5ab2' }}>
{shareLimitDate}
</Box>
&nbsp;
<Box component="span" sx={{ fontWeight: 600, color: '#0f5ab2' }}>
</Box>
.
</Typography>
<Stack direction="row" justifyContent="space-between" mt="18px">
<TextField sx={{ width: '298px', height: '32px' }} disabled value={shareLink} />
<Button variant="contained" sx={{ minWidth: '55px' }} onClick={handleCopyClick}>
</Button>
</Stack>
</Stack>
</Paper>
) : (
<Paper sx={{ ...paperSx, width: '243px' }} ref={ref}>
<Stack sx={{ width: '100%', p: '22px 24px 20px' }}>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleShareToggle();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: '22px' }}
>
<Typography sx={{ fontSize: '16px', fontWeight: 600, color: '#141414' }}> </Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOn />
</IconButton>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography sx={{ fontSize: '14px', color: '#141414' }}>
:
<Box component="span" sx={{ ml: '4px', fontWeight: 'bold', color: '#333' }}>
{shareLimitDate}
</Box>
</Typography>
<Button
variant="contained"
sx={{ width: '32px', minWidth: '32px', height: '32px', p: 0 }}
onClick={handleCopyClick}
>
<IconLink />
</Button>
</Stack>
</Stack>
</Paper>
);
});
const ShareOffPopup = forwardRef((props: SharePopupProps, ref: Ref<HTMLDivElement>) => {
const { matches, handleShareToggle, shareLimitDate, setShareLimitDate } = props;
return matches ? (
<Paper sx={{ ...paperSx, minWidth: '410px' }} ref={ref}>
<Stack sx={{ width: '100%', padding: '24px' }}>
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}> </Typography>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleShareToggle();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
mb="16px"
>
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
.
</Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOff />
</IconButton>
</Stack>
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
<Typography component="span" mr="8px">
:
</Typography>
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
</Stack>
</Stack>
</Paper>
) : (
<Paper sx={{ ...paperSx, width: '243px' }} ref={ref}>
<Stack sx={{ width: '100%', p: '22px 24px 20px' }}>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleShareToggle();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: '22px' }}
>
<Typography sx={{ fontSize: '16px', fontWeight: 600, color: '#141414' }}> </Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOff />
</IconButton>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography sx={{ fontSize: '14px', color: '#141414' }}> :</Typography>
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
</Stack>
</Stack>
</Paper>
);
});
const shareOnButtonSx: SxProps = {
flexShrink: 0,
border: '1px solid #0f5ab2',
backgroundColor: '#fff',
color: '#0f5ab2',
fill: '#0f5ab2',
'& span': { mr: '6px' },
'&:hover': { color: '#fff', fill: '#fff' },
};
const shareOffButtonSx: SxProps = {
flexShrink: 0,
fill: '#fff',
'& span': { mr: '6px' },
'&:hover': { border: '1px solid #0f5ab2', backgroundColor: '#fff', color: '#0f5ab2', fill: '#0f5ab2' },
};
function ShareButton(props: ShareButtonProps) {
const { handleShareToggle, isShareOn, shareId, shareLimitDate, setShareLimitDate } = props;
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const ref: Ref<HTMLDivElement> = useRef();
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = event => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(null);
};
return (
<React.Fragment>
{isShareOn ? (
<Button
startIcon={<IconShare fill="inherit" />}
variant="contained"
sx={{
px: '12px',
border: '1px solid #0f5ab2',
backgroundColor: '#fff',
color: '#0f5ab2',
fill: '#0f5ab2',
'& span': { mr: '6px' },
'&:hover': { color: '#fff', fill: '#fff' },
}}
onClick={handleClick}
sx={{
...shareOnButtonSx,
px: { xs: '8px', sm: '12px' },
}}
>
</Button>
@@ -57,107 +249,38 @@ const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShar
<Button
startIcon={<IconShare fill="inherit" />}
variant="contained"
sx={{
px: '12px',
fill: '#fff',
'& span': { mr: '6px' },
'&:hover': { border: '1px solid #0f5ab2', backgroundColor: '#fff', color: '#0f5ab2', fill: '#0f5ab2' },
}}
onClick={handleClick}
sx={{
...shareOffButtonSx,
px: { xs: '8px', sm: '12px' },
}}
>
</Button>
)}
<Popper id={id} open={open} anchorEl={anchorEl} disablePortal={false} placement="bottom-end">
<ClickAwayListener onClickAway={handleClose}>
<Paper
sx={{
display: 'flex',
gap: '32px',
minWidth: '410px',
width: '410px',
mt: '3px',
p: '24px',
border: 'solid 1px #ddd',
borderRadius: '6px',
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
}}
>
<Stack sx={{ width: '100%' }}>
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}> </Typography>
{isShareOn ? (
<Box>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleSubmit();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
mb="3px"
>
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
.
</Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOn />
</IconButton>
</Stack>
<Typography>
&nbsp;
<Box component="span" sx={{ color: '#0f5ab2' }}>
{shareLimitDate}
</Box>
&nbsp;
<Box component="span" sx={{ fontWeight: 600, color: '#0f5ab2' }}>
</Box>
.
</Typography>
<Stack direction="row" justifyContent="space-between" mt="18px">
<TextField sx={{ width: '298px', height: '32px' }} disabled value={shareLink} />
<Button variant="contained" sx={{ minWidth: '55px' }} onClick={handleCopyClick}>
</Button>
</Stack>
</Box>
) : (
<Box>
<Stack
component="form"
onSubmit={event => {
event.preventDefault();
handleSubmit();
}}
direction="row"
justifyContent="space-between"
alignItems="center"
mb="16px"
>
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
.
</Typography>
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
<IconToggleOff />
</IconButton>
</Stack>
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
<Typography component="span" mr="8px">
:
</Typography>
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
</Stack>
</Box>
)}
</Stack>
</Paper>
{isShareOn ? (
<ShareOnPopup
ref={ref}
matches={matches}
handleShareToggle={handleShareToggle}
shareLimitDate={shareLimitDate}
shareId={shareId}
/>
) : (
<ShareOffPopup
ref={ref}
matches={matches}
handleShareToggle={handleShareToggle}
shareLimitDate={shareLimitDate}
setShareLimitDate={setShareLimitDate}
/>
)}
</ClickAwayListener>
</Popper>
</React.Fragment>
);
};
}
export default ShareButton;

View File

@@ -1,6 +1,7 @@
import Grid from '@toast-ui/react-grid';
import React, { HTMLAttributes, useContext, useEffect, useRef } from 'react';
import React, { forwardRef, HTMLAttributes, PropsWithChildren, useEffect, useRef } from 'react';
import { GridEventListener, GridOptions } from 'tui-grid';
import { Stack } from '@mui/material';
type EventNameMapping = {
onClick: 'click';
@@ -37,8 +38,36 @@ type Props = Omit<GridOptions, 'el'> &
oneTimeBindingProps?: Array<'data' | 'columns' | 'bodyHeight' | 'frozenColumnCount'>;
};
const DataGrid = (props: Props) => {
const gridRef = useRef();
export const DataGridWrapper = forwardRef((props: PropsWithChildren, ref: React.Ref<HTMLDivElement>) => {
const { children } = props;
return (
<Stack
ref={ref}
sx={{
flex: 1,
height: '100%',
minHeight: '100%',
overflow: 'hidden',
}}
>
{children}
</Stack>
);
});
interface DataGridProps extends Props {
resizeObserver?: any;
}
const DataGrid = (props: DataGridProps) => {
const gridRef = useRef<Grid>();
const { resizeObserver, ...rest } = props;
useEffect(() => {
if (resizeObserver) {
gridRef.current.getInstance().refreshLayout();
}
}, [resizeObserver]);
return (
<Grid
@@ -49,12 +78,12 @@ const DataGrid = (props: Props) => {
}}
rowHeight={36}
minRowHeight={36}
minBodyHeight={300}
minBodyHeight={100}
usageStatistics={true}
columnOptions={{
resizable: true,
}}
{...props}
{...rest}
/>
);
};

View File

@@ -3,15 +3,25 @@ import TextField from '@mui/material/TextField';
import 'dayjs/locale/ko';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
console.log(shareLimitDate);
// TODO: MobileDatePicker 활성화시 ClickAwayListener 간섭 버그 해결
// import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
import { DesktopDatePicker as MuiDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
interface DatePickerProps {
shareLimitDate: string;
setShareLimitDate: React.Dispatch<React.SetStateAction<any>>;
}
const DatePicker = (props: DatePickerProps) => {
const { shareLimitDate, setShareLimitDate } = props;
return (
<LocalizationProvider adapterLocale="ko" dateAdapter={AdapterDayjs}>
<MuiDatePicker
disablePast
closeOnSelect={false}
value={shareLimitDate}
onChange={newValue => {
onChange={(newValue: any) => {
const selectDate = new Date(Date.parse(newValue.$d)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
@@ -20,7 +30,6 @@ const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
setShareLimitDate(selectDate);
}}
renderInput={params => {
console.log(params, 'ddd');
return (
<TextField
{...params}
@@ -35,8 +44,8 @@ const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
/>
);
}}
disablePast
PopperProps={{
disablePortal: true,
sx: {
'& .MuiPickersDay-root': {
fontFamily: 'Pretendard',

View File

@@ -1,56 +1,61 @@
import { Card, Grid } from '@mui/material';
import { Stack, SxProps } from '@mui/material';
import React from 'react';
const CardListWrapper = ({ children, minWidth }) => {
interface CardListWrapperProps {
children: React.ReactNode;
sx?: SxProps;
}
interface CardWrapperProps {
children: React.ReactNode;
sx?: SxProps;
handleClick?: (item) => void;
}
export const CardListWrapper = (props: CardListWrapperProps) => {
const { children, sx } = props;
return (
<Grid
container
spacing={2}
<Stack
component="ul"
sx={{
display: { xs: 'flex', md: 'grid' },
gridTemplateColumns: { xs: 'repeat(100%)', sm: 'repeat(auto-fit, minmax(0, 228px))' },
gap: '8px',
minHeight: '20px',
listStyle: 'none',
pl: 0,
display: 'grid',
gridTemplateColumns: `repeat(${minWidth || 'auto-fit, minmax(0, 228px)'})`,
...sx,
}}
>
{children}
</Grid>
</Stack>
);
};
CardListWrapper.defaultProps = {
children: '',
minWidth: false,
};
export default CardListWrapper;
export const CardWrapper = ({ children, selected, onClick, sx = null }) => {
export const CardWrapper = (props: CardWrapperProps) => {
const { children, sx, handleClick } = props;
return (
<Card
sx={{
padding: '20px 8px 20px 21px',
borderRadius: '8px',
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
backgroundColor: selected ? '#edf8ff' : '#fff',
cursor: 'pointer',
position: 'relative',
alignItems: 'center',
'&:hover': {
backgroundColor: '#ebfbff',
},
...sx,
}}
onClick={onClick}
>
{children}
</Card>
<Stack component="li" sx={{ flex: '1 1 auto' }}>
<Stack
component={handleClick ? 'button' : 'div'}
sx={{
display: 'flex',
flexDirection: 'column',
padding: '20px',
borderRadius: '8px',
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
border: 'solid 1px #ddd',
backgroundColor: '#fff',
...(handleClick ? { '&:hover': { backgroundColor: '#ebfbff' } } : {}),
...sx,
}}
onClick={handleClick}
>
{children}
</Stack>
</Stack>
);
};
CardListWrapper.defaultProps = {
children: '',
minWidth: false,
};

View File

@@ -1,96 +1,99 @@
import React, { Fragment } from 'react';
import { CardActions, CardContent, Grid, Typography } from '@mui/material';
import React from 'react';
import { Box, Stack, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import CardListWrapper, { CardWrapper } from '@/components/list/CardListWrapper';
import { any } from 'prop-types';
import { ReactComponent as IconDatabase } from '@/assets/images/icon/ic-data.svg';
import DeleteButton from '@/components/button/DeleteButton';
import ModifyButton from '@/components/button/ModifyButton';
import { DatabaseProps } from '@/pages/Data/DataLayout';
import { ReactComponent as IconDatabase } from '@/assets/images/icon/ic-data.svg';
export const DatabaseCardList = props => {
const { data, onUpdate, selectedDatabase, minWidth, disabledIcons, onRemove } = props;
interface DatabaseCardListProps {
data: DatabaseProps[];
selectedData: DatabaseProps;
handleDataClick?: (item) => void;
handleDataRemove?: (item) => void;
isViewMode?: boolean;
icon?: React.ReactNode;
}
const handleClick = id => {
if (onUpdate !== undefined) {
onUpdate({ databaseId: id });
}
};
const selectedSx = { border: 'solid 1px #0f5ab2', backgroundColor: '#edf8ff' };
const selectedIconSx = { fill: '#0f5ab2' };
const selectedTypoSx = { fontWeight: 'bold', color: '#0f5ab2' };
export const DatabaseCardList = (props: DatabaseCardListProps) => {
const { data, selectedData, handleDataClick, handleDataRemove, isViewMode, icon = <IconDatabase /> } = props;
return (
<CardListWrapper minWidth={minWidth}>
{data.map
? data.map(item => {
const selected = selectedDatabase.databaseId == item.id;
return (
<Grid item xs={12} md component="li" key={item.id}>
<CardWrapper selected={selected} onClick={() => handleClick(item.id)}>
<CardContent sx={{ p: '0 !important', alignItems: 'center', display: 'flex' }}>
<IconDatabase />
<Typography
component="span"
variant="subtitle2"
sx={{
pl: '10px',
width: disabledIcons ? '100%' : '70%',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
>
{item.name}
</Typography>
</CardContent>
<CardListWrapper sx={{ gridTemplateColumns: 'repeat(100%)' }}>
{data.length > 0 &&
data.map(item => (
<CardWrapper
key={item.id}
sx={{
flexDirection: 'row',
alignItems: 'center',
height: '62px',
...(selectedData?.id == item.id && selectedSx),
}}
handleClick={() => handleDataClick(item)}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
mr: '8px',
fill: '#767676',
...(selectedData?.id == item.id && selectedIconSx),
}}
>
{icon}
</Box>
<Typography
variant="subtitle2"
sx={{
flex: 1,
width: '100%',
mr: '18px',
textAlign: 'left',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
color: '#333',
...(selectedData?.id == item.id && selectedTypoSx),
}}
>
{item.name}
</Typography>
{/* 아이콘 */}
{disabledIcons ? (
<Fragment />
) : (
<CardActions
disableSpacing
sx={{
position: 'absolute',
top: 0,
bottom: 0,
right: 10,
display: 'flex',
justifyContent: 'flex-end,',
m: 0,
p: 0,
}}
onClick={handleClick}
>
<ModifyButton
size="medium"
component={RouterLink}
to={`/data/source/modify/${item.id}`}
width="20"
height="20"
fill="#767676"
/>
<DeleteButton
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
onRemove(item.id, item.name);
}}
width="20"
height="20"
fill="#767676"
/>
</CardActions>
)}
</CardWrapper>
</Grid>
);
})
: ''}
{!isViewMode && (
<Stack direction="row" justifyContent="flex-end" width="100%" flex={0}>
<ModifyButton
size="medium"
component={RouterLink}
to={`/data/source/modify/${item.id}`}
width="20"
height="20"
padding="0"
fill="#767676"
sx={{ p: 0, mr: '18px' }}
/>
<DeleteButton
component="span"
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDataRemove(item);
}}
width="20"
height="20"
fill="#767676"
sx={{ p: 0 }}
/>
</Stack>
)}
</CardWrapper>
))}
</CardListWrapper>
);
};
DatabaseCardList.defaultProps = {
data: any,
minWidth: false,
disabledIcons: false,
};

View File

@@ -1,92 +1,81 @@
import { CardActions, CardContent, Grid, Typography } from '@mui/material';
import { Stack, SxProps, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import React from 'react';
import CardListWrapper, { CardWrapper } from '@/components/list/CardListWrapper';
import { any } from 'prop-types';
import ModifyButton from '@/components/button/ModifyButton';
import DeleteButton from '@/components/button/DeleteButton';
import { DataSetProps, DataTableProps } from '@/pages/Data/DataLayout';
export const DatasetCardList = props => {
const { data, minWidth, disabledIcons, selectedDataset, onSelectDataset, onDeleteDataset } = props;
interface DatasetCardListProps {
data: DataSetProps[] | DataTableProps[];
selectedData: DataSetProps | DataTableProps;
handleDataClick?: (item) => void;
handleDataRemove?: (item) => void;
sx?: SxProps;
isViewMode?: boolean;
}
const selectedSx = { border: 'solid 1px #0f5ab2', backgroundColor: '#edf8ff' };
export const DatasetCardList = (props: DatasetCardListProps) => {
const { data, selectedData, handleDataClick, handleDataRemove, isViewMode } = props;
return (
<CardListWrapper minWidth={minWidth}>
{data.map
? data.map(item => {
const selected = selectedDataset?.id == item.id;
return (
<Grid item component="li" key={item.id}>
<CardWrapper selected={selected} onClick={() => onSelectDataset(item)}>
<CardContent
sx={{
p: '0 !important',
display: 'flex',
alignItems: 'center',
}}
>
<Typography
component="span"
variant="subtitle2"
sx={{
width: { xs: '50%', md: '100%' },
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
>
{item.title || item.tableName}
</Typography>
</CardContent>
<CardListWrapper>
{data.length > 0 &&
data.map(item => (
<CardWrapper
key={item.id}
sx={{
maxHeight: '90px',
...(isViewMode && selectedData?.id == item.id && selectedSx),
}}
handleClick={() => handleDataClick(item)}
>
<Typography
variant="subtitle2"
sx={{
width: '100%',
textAlign: 'left',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
color: '#333',
}}
>
{item.title || item.tableName}
</Typography>
{/* 아이콘 */}
{disabledIcons ? (
<></>
) : (
<CardActions
disableSpacing
sx={{
top: 0,
bottom: 0,
right: 10,
display: 'flex',
justifyContent: 'end',
width: '100%',
m: 0,
p: 0,
}}
>
<ModifyButton
size="medium"
component={RouterLink}
to={`/data/set/modify/${item.id}`}
width="20"
height="20"
fill="#767676"
/>
<DeleteButton
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
onDeleteDataset(item);
}}
width="20"
height="20"
fill="#767676"
/>
</CardActions>
)}
</CardWrapper>
</Grid>
);
})
: ''}
{/* 아이콘 */}
{!isViewMode && (
<Stack direction="row" justifyContent="flex-end" width="100%" mt="11px">
<ModifyButton
size="medium"
component={RouterLink}
to={`/data/set/modify/${item.id}`}
width="20"
height="20"
padding="0"
fill="#767676"
sx={{ p: 0, mr: '18px' }}
/>
<DeleteButton
component="span"
size="medium"
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDataRemove(item);
}}
width="20"
height="20"
fill="#767676"
sx={{ p: 0 }}
/>
</Stack>
)}
</CardWrapper>
))}
</CardListWrapper>
);
};
DatasetCardList.defaultProps = {
data: any,
minWidth: false,
disabledIcons: false,
};

View File

@@ -4,6 +4,12 @@ import { Box } from '@mui/material';
import { ReactElement } from 'react';
import loadingGif from '@/assets/images/loading.gif';
interface LoadingProps {
in: boolean;
style?: React.CSSProperties;
rest?: any;
}
const duration = 100;
const defaultStyle = {
@@ -32,7 +38,7 @@ const LoadingBox = styled(Box)(() => ({
zIndex: 100,
}));
export const Loading = ({ in: inProp, ...rest }): ReactElement => {
export const Loading = ({ in: inProp, style, ...rest }: LoadingProps): ReactElement => {
return (
<Transition in={inProp} timeout={duration}>
{state => (
@@ -40,6 +46,7 @@ export const Loading = ({ in: inProp, ...rest }): ReactElement => {
style={{
...defaultStyle,
...transitionStyles[state],
...style,
}}
{...rest}
>

View File

@@ -6,7 +6,8 @@ import { useAlert } from 'react-alert';
import { useNavigate } from 'react-router-dom';
import { removeToken } from '@/helpers/authHelper';
const Logout = () => {
const Logout = props => {
const { sx } = props;
const { showLoading, hideLoading } = useContext(LoadingContext);
const navigate = useNavigate();
const alert = useAlert();
@@ -53,29 +54,18 @@ const Logout = () => {
disableFocusRipple
disableTouchRipple
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minWidth: 0,
minHeight: 0,
m: 0,
p: 0,
gap: '12px',
fontSize: 'inherit',
fontWeight: 'inherit',
textDecoration: 'underline',
color: 'inherit',
whiteSpace: 'nowrap',
'&:hover': {
textDecoration: 'underline',
backgroundColor: 'inherit',
},
'&:after': {
content: `""`,
width: '1px',
height: '10px',
backgroundColor: '#cccfd8',
},
...sx,
}}
>

View File

@@ -63,22 +63,29 @@ const ProfileViewButton = () => {
{userState.userEmail}
</Typography>
<Stack>
<Typography
component="div"
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '12px',
mt: '32px',
fontSize: '14px',
textAlign: 'center',
color: '#4a4a4a',
}}
>
<Logout />
<Logout
sx={{
textDecoration: 'underline',
'&:hover': {
backgroundColor: 'transparent',
textDecoration: 'underline',
},
}}
/>
<Box sx={{ width: '1px', height: '10px', mx: '12px', backgroundColor: '#cccfd8' }} />
<ProfileModify />
</Typography>
</Box>
</Stack>
</Stack>
</Paper>

View File

@@ -106,4 +106,9 @@ export const LEGEND_LIST = [
export const STATUS = {
SUCCESS: 'SUCCESS',
};
PENDING: 'PENDING',
FAILURE: 'FAILURE',
FINISH: 'FINISH',
} as const;
export const MAX_WIDTH = '1392px';

View File

@@ -1,15 +1,25 @@
import { createContext, FC, useState } from 'react';
import { createContext, useState } from 'react';
type LayoutContext = { fixed: boolean; fixLayout: (visible: boolean) => void };
type LayoutContext = {
fixed: boolean;
fixLayout: (visible: boolean) => void;
footerBg: string | null;
changeFooterBg: (color: string) => void;
};
export const LayoutContext = createContext<LayoutContext>({} as LayoutContext);
export const LayoutProvider = ({ children }) => {
const [fixed, setLayoutFix] = useState(false);
const [footerBg, setFooterBg] = useState(null);
const fixLayout = (visible: boolean) => {
setLayoutFix(visible);
};
return <LayoutContext.Provider value={{ fixed, fixLayout }}>{children}</LayoutContext.Provider>;
const changeFooterBg = (color: string) => {
setFooterBg(color);
};
return <LayoutContext.Provider value={{ fixed, fixLayout, footerBg, changeFooterBg }}>{children}</LayoutContext.Provider>;
};

View File

@@ -13,12 +13,42 @@ const instance = axios.create({
headers: { 'Content-Type': 'application/json' },
});
let pendingRequests = {};
let isLoginUser = true;
let isAlreadyFetchingAccessToken = false;
let subscribers: ((token: string) => void)[] = [];
// console.log('pendingRequests', pendingRequests);
// request interceptor
instance.interceptors.request.use(async config => {
// 요청에 대한 unique key 생성
const generateReqKey = config => {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
};
// 진행중인 요청 저장
const addPendingRequest = config => {
const requestKey = generateReqKey(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingRequests[requestKey]) {
pendingRequests[requestKey] = [];
}
pendingRequests[requestKey].push(cancel);
});
};
// 저장된 요청 취소
const removePendingRequest = config => {
const requestKey = generateReqKey(config);
if (pendingRequests[requestKey]) {
pendingRequests[requestKey].forEach(cancel => {
cancel('Request canceled due to new request.');
});
delete pendingRequests[requestKey];
}
};
// 토큰 정보 요청 header에 삽입
const addAuthToHeaders = config => {
const token = getToken();
const shareToken = getShareToken();
@@ -30,40 +60,56 @@ instance.interceptors.request.use(async config => {
config.headers['Authorization-url'] = `Bearer ${shareToken}`;
}
return config;
};
// 요청 인터셉터
instance.interceptors.request.use(async config => {
const newConfig = addAuthToHeaders(config);
// removePendingRequest(newConfig); // 같은 요청이 갔을 경우 기존 요청 취소
// addPendingRequest(newConfig);
return newConfig;
});
// response interceptor
// 중단 이벤트 발생 시 모든 요청 중단
export function cancelAllRequests() {
Object.keys(pendingRequests).forEach(key => {
pendingRequests[key].forEach(cancel => {
cancel('All requests canceled');
});
});
pendingRequests = {};
}
let isAlreadyFetchingAccessToken = false;
const subscribers: ((token: string) => void)[] = [];
// 응답 인터셉터
instance.interceptors.response.use(
response => {
// removePendingRequest(response.config); // 완료된 요청 삭제
return response;
},
async error => {
const {
response: { status },
} = error;
if (status === 401) {
if (error.response.data.data === 'accessTokenExpired' && isLoginUser) {
// 로그인 사용자의 token 만료 후 첫 요청
return await resetTokenAndReattemptRequest(error);
}
const { response: errorResponse } = error;
if (errorResponse?.status === 401 && errorResponse?.data?.message === 'accessTokenExpired' && isLoginUser) {
// 로그인 사용자의 token 만료 후 첫 요청
await resetTokenAndReattemptRequest(errorResponse);
}
error.message =
(error.response && error.response.data && error.response.data.message) || error.message || error.toString();
(errorResponse && errorResponse?.data && errorResponse?.data?.message) || error?.message || error.toString();
return Promise.reject(error);
},
);
async function resetTokenAndReattemptRequest(error) {
async function resetTokenAndReattemptRequest(errorResponse) {
// subscribers에 access token을 받은 이후 재요청할 함수 추가 (401로 실패했던)
// retryOriginalRequest는 pending 상태로 있다가
// access token을 받은 이후 onAccessTokenFetched가 호출될 때
// access token을 넘겨 다시 axios로 요청하고
// 결과값을 처음 요청했던 promise의 resolve로 settle시킨다.
try {
const { response: errorResponse } = error;
// subscribers에 access token을 받은 이후 재요청할 함수 추가 (401로 실패했던)
// retryOriginalRequest는 pending 상태로 있다가
// access token을 받은 이후 onAccessTokenFetched가 호출될 때
// access token을 넘겨 다시 axios로 요청하고
// 결과값을 처음 요청했던 promise의 resolve로 settle시킨다.
const retryOriginalRequest = new Promise((resolve, reject) => {
addSubscriber(async accessToken => {
subscribers.push(async accessToken => {
try {
errorResponse.config.headers.Authorization = accessToken;
resolve(instance(errorResponse.config));
@@ -84,10 +130,8 @@ async function resetTokenAndReattemptRequest(error) {
setToken(newAccessToken);
isAlreadyFetchingAccessToken = false; // 문열기 (초기화)
onAccessTokenFetched(data.accessToken);
}
return retryOriginalRequest; // pending 됐다가 onAccessTokenFetched가 호출될 때 resolve
} catch (error) {
console.log('error', error);
@@ -99,13 +143,9 @@ async function resetTokenAndReattemptRequest(error) {
}
}
function addSubscriber(callback) {
subscribers.push(callback);
}
function onAccessTokenFetched(accessToken) {
subscribers.forEach(callback => callback(accessToken));
subscribers = [];
subscribers.length = 0;
}
export async function get(url, data?, config = {}) {
@@ -124,10 +164,6 @@ export async function del(url, config = {}) {
return instance.delete(url, { ...config });
}
export async function postForm(url, data?, config = {}) {
return instance.post(url, data, { ...config });
}
export async function patch(url, data?, config = {}) {
return instance.patch(url, { ...data }, { ...config });
}

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { Link, Stack, Typography } from '@mui/material';
const Footer = props => {
const { height } = props;
return (
<Stack sx={{ height: height, alignItems: 'center', justifyContent: 'center' }}>
<Link
color="inherit"
href="https://vanillabrain.com/"
target="_blank"
sx={{ fontSize: '13px', color: '#767676', textDecoration: 'none' }}
>
<Typography
sx={{
width: '100%',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '13px',
fontWeight: 'normal',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 'normal',
letterSpacing: 'normal',
textAlign: 'center',
color: '#767676',
}}
>
VanillaBrain Inc.
</Typography>
</Link>
</Stack>
);
};
export default Footer;

View File

@@ -0,0 +1,37 @@
import React, { useContext } from 'react';
import { Link, Stack, Typography } from '@mui/material';
import { LayoutContext } from '@/contexts/LayoutContext';
export const Copyright = (props: any) => {
return (
<Typography color="text.secondary" align="center" {...props}>
<Link
color="inherit"
href="https://vanillabrain.com/"
target="_blank"
sx={{ fontSize: '13px', color: '#767676', fontWeight: 'bold', textDecoration: 'none' }}
>
VanillaBrain Inc.
</Link>
</Typography>
);
};
const Footer = () => {
const { footerBg } = useContext(LayoutContext);
return (
<Stack
sx={{
height: '50px',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: footerBg ? footerBg : '#fff',
}}
>
<Copyright />
</Stack>
);
};
export default Footer;

View File

@@ -30,17 +30,19 @@ function NavBar(props) {
component={NavLink}
to={item.link}
sx={{
minWidth: 0,
color: 'inherit',
padding: 0,
fontSize: 15,
fontSize: { xs: 13, sm: 15 },
height: 50,
px: '18px',
px: { xs: '14px', sm: '18px' },
fontFamily: 'Pretendard',
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 'normal',
letterSpacing: 'normal',
whiteSpace: 'nowrap',
}}
>
{item.name}

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { AppBar, Box, Divider, Toolbar } from '@mui/material';
import { AppBar, Box, Divider, Hidden, Toolbar } from '@mui/material';
import { AddMenuIconButton } from '@/components/button/AddIconButton';
import Logo from './Logo';
import NavBar from './NavBar';
import { useNavigate } from 'react-router-dom';
import ProfileViewButton from '@/components/user/ProfileViewButton';
import Logout from '@/components/user/Logout';
const menuList = [
{ name: '데이터 소스', link: '/data/source/create' },
@@ -13,8 +14,7 @@ const menuList = [
{ name: '대시보드', link: '/dashboard/create?createType=dashboard' },
];
function Header(props) {
const headerHeight = props.height;
function Header() {
const navigate = useNavigate();
const navItems = [
@@ -24,20 +24,26 @@ function Header(props) {
];
const handleMenuSelect = item => {
if (item.link !== undefined) {
if (item.link) {
navigate(item.link);
}
};
return (
<AppBar elevation={0} component="nav" sx={{ minWidth: '600px', left: 0, height: '65px' }}>
<Toolbar variant="dense" sx={{ height: headerHeight, justifyContent: 'space-between', columnGap: '30px' }}>
<AppBar elevation={0} component="nav" sx={{ left: 0, height: { xs: '56px', sm: '65px' } }}>
<Toolbar variant="dense" sx={{ height: 65, justifyContent: 'space-between', columnGap: { xs: '20px', sm: '32px' } }}>
<Logo />
<NavBar navItems={navItems} />
<Box sx={{ display: 'flex', gap: '16px' }}>
<AddMenuIconButton menuList={menuList} handleSelect={handleMenuSelect} />
<ProfileViewButton />
</Box>
<Hidden smDown>
<NavBar navItems={navItems} />
<Box sx={{ display: 'flex', gap: '16px' }}>
<AddMenuIconButton menuList={menuList} handleSelect={handleMenuSelect} />
<ProfileViewButton />
</Box>
</Hidden>
<Hidden smUp>
<NavBar navItems={navItems.slice(0, 2)} />
<Logout sx={{ fontSize: '12px', color: '#767676' }} />
</Hidden>
</Toolbar>
<Divider />
</AppBar>

View File

@@ -1,46 +0,0 @@
import React, { useContext } from 'react';
import { Box, Stack } from '@mui/material';
import Header from './Header/Header';
import Footer from './Footer/Footer';
import { LayoutContext } from '@/contexts/LayoutContext';
import { Outlet } from 'react-router-dom';
const Layout = props => {
const { children } = props;
const headerHeight = 65;
const footerHeight = 50;
const { fixed } = useContext(LayoutContext);
const defaultSx = {
width: '100%',
height: '100%',
};
const fixSx = {
width: '100%',
height: '100%',
overflow: 'hidden',
flex: '1 1 auto',
};
return (
<Box sx={fixed ? fixSx : defaultSx}>
<Header height={headerHeight} />
<Stack
sx={{
width: '100%',
minWidth: 'min-content',
paddingTop: headerHeight + 'px',
minHeight: `calc(100% - ${footerHeight}px)`,
}}
>
{children || <Outlet />}
</Stack>
<Footer height={footerHeight} />
</Box>
);
};
export default Layout;

View File

@@ -0,0 +1,38 @@
import React, { useContext } from 'react';
import { Box, Stack } from '@mui/material';
import Header from '@/layouts/Header';
import Footer from '@/layouts/Footer';
import { LayoutContext } from '@/contexts/LayoutContext';
import { Outlet } from 'react-router-dom';
const Layout = props => {
const { children } = props;
const { fixed, footerBg } = useContext(LayoutContext);
return (
<Box
sx={{
width: '100%',
height: '100%',
flex: '1 1 auto',
overflow: fixed ? 'hidden' : 'visible',
backgroundColor: footerBg ? footerBg : '#fff',
}}
>
<Header />
<Stack
sx={{
flex: '1 1 auto',
width: '100%',
pt: { xs: '56px', sm: '65px' },
minHeight: `calc(100% - 50px)`,
}}
>
{children || <Outlet />}
</Stack>
<Footer />
</Box>
);
};
export default Layout;

View File

@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Box, Stack } from '@mui/material';
import Footer from '@/layouts/Footer';
import { Outlet } from 'react-router-dom';
import { LandingLogo } from '@/layouts/Header/Logo';
import { MAX_WIDTH } from '@/constant';
import { LayoutContext } from '@/contexts/LayoutContext';
const PublicLayout = props => {
const { children } = props;
const { fixed, footerBg } = useContext(LayoutContext);
return (
<Box
sx={{
width: '100%',
height: '100%',
flex: '1 1 auto',
overflow: fixed ? 'hidden' : 'visible',
backgroundColor: footerBg ? footerBg : '#fff',
}}
>
<Stack sx={{ width: '100%', maxWidth: MAX_WIDTH, mx: 'auto', backgroundColor: '#fff' }}>
<Stack sx={{ height: 65, justifyContent: 'center', pl: '20px' }}>
<LandingLogo />
</Stack>
{children || <Outlet />}
</Stack>
<Footer />
</Box>
);
};
export default PublicLayout;

View File

@@ -176,7 +176,7 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
<ListItemButton
key={index}
selected={isItemSelection(item)}
sx={{ paddingX: '20px', height: '50px' }}
sx={{ width: '100%', paddingX: '20px', height: '50px' }}
onClick={() => handleClick(item)}
>
<Checkbox checked={isItemSelection(item)} />
@@ -198,7 +198,15 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{
sx: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
}}
sx={{
// maxWidth: '470px',
marginLeft: '16px',
fontFamily: 'Pretendard',
fontSize: '14px',

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { Box, Divider, Stack } from '@mui/material';
function DashboardTitleBox(props) {
const { title, button, sx } = props;
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Box
sx={{
width: '1392px',
minWidth: '1392px',
height: '100%',
borderRadius: '6px',
border: 'solid 1px #ddd',
backgroundColor: '#f9f9fa',
...sx,
}}
>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ width: '100%', height: '57px', backgroundColor: '#ffffff', borderRadius: '6px 6px 0px 0px' }}
>
{title}
{button}
</Stack>
<Divider sx={{ width: '100%', height: '1px' }} />
{props.children}
</Box>
</Box>
);
}
DashboardTitleBox.defaultProps = {
title: '',
width: '100%',
menuList: false,
naviUrl: false,
button: false,
};
export default DashboardTitleBox;

View File

@@ -19,7 +19,7 @@ import {
import { useAlert } from 'react-alert';
import TemplateService from '@/api/templateService';
import WidgetService from '@/api/widgetService';
import { STATUS } from '@/constant';
import { MAX_WIDTH, STATUS } from '@/constant';
import CloseButton from '@/components/button/CloseButton';
import { ReactComponent as TemplateIcon01 } from '@/assets/images/template/template01.svg';
import { ReactComponent as TemplateIcon02 } from '@/assets/images/template/template02.svg';
@@ -186,6 +186,13 @@ export const WidgetList = ({
/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{
sx: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
}}
sx={{
marginLeft: '16px',
fontFamily: 'Pretendard',
@@ -446,7 +453,7 @@ function RecommendDashboardPopup({ recommendOpen = false, handleComplete = null
setDialogWidth('600px');
} else if (step == 2) {
setSubTitle('템플릿을 선택하세요');
setDialogWidth('1392px');
setDialogWidth(MAX_WIDTH);
}
}, [step]);

View File

@@ -1,7 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Card, Stack, TextField } from '@mui/material';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import AddWidgetPopup from '@/pages/Dashboard/Components/AddWidgetPopup';
import ConfirmCancelButton from '@/components/button/ConfirmCancelButton';
@@ -16,7 +15,7 @@ import AddIcon from '@mui/icons-material/Add';
import RecommendDashboardPopup from '@/pages/Dashboard/Components/RecommendDashboardPopup';
import DashboardService from '@/api/dashboardService';
import { STATUS } from '@/constant';
import DashboardTitleBox from '../Components/DashboardTitleBox';
import PageViewBox from '../../../components/PageViewBox';
import CloseButton from '@/components/button/CloseButton';
import bg from '@/assets/images/dashboard-bg.svg';
import { LoadingContext } from '@/contexts/LoadingContext';
@@ -346,129 +345,126 @@ function DashboardModify() {
};
return (
<PageContainer>
<PageTitleBox
upperTitle="대시보드"
upperTitleLink="/dashboard"
title={topTitle}
sx={{ width: '100%', marginTop: '22px' }}
button={
<Stack direction="row" spacing={3} sx={{ marginRight: '20px' }}>
<ConfirmCancelButton
confirmProps={{ disabled: false, onClick: handleSaveDialogSelect }}
cancelProps={{ onClick: handleCancelDialogSelect }}
/>
</Stack>
}
>
<DashboardTitleBox
title={
<TextField
id="userDashboardName"
label="대시보드 이름"
required
sx={{
width: '960px',
height: '32px',
marginLeft: '16px',
marginTop: 0,
borderRadius: '4px',
backgroundColor: '#fff',
input: {
fontWeight: 500,
paddingLeft: '18px',
<PageTitleBox
upperTitle="대시보드"
upperTitleLink="/dashboard"
title={topTitle}
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
button={
<Stack direction="row" spacing={3} sx={{ marginRight: '20px' }}>
<ConfirmCancelButton
confirmProps={{ disabled: false, onClick: handleSaveDialogSelect }}
cancelProps={{ onClick: handleCancelDialogSelect }}
/>
</Stack>
}
>
<PageViewBox
titleElement={
<TextField
id="userDashboardName"
label="대시보드 이름"
required
sx={{
width: { xs: '100%', sm: '960px' },
height: '32px',
marginLeft: { sm: '16px' },
marginTop: 0,
borderRadius: '4px',
backgroundColor: '#fff',
input: {
fontWeight: 500,
paddingLeft: '18px',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '14px',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 0.89,
letterSpacing: '-0.18px',
textAlign: 'left',
color: '#141414',
'&::placeholder': {
height: '16px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: 'normal',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 0.89,
letterSpacing: '-0.18px',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#141414',
'&::placeholder': {
height: '16px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: 'normal',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#929292',
opacity: 1,
},
color: '#929292',
opacity: 1,
},
}}
placeholder="대시보드의 이름을 입력해 주세요"
value={dashboardTitle}
onChange={event => {
setDashboardTitle(event.target.value);
}}
/>
}
button={
<>
<Button
onClick={handleWidgetOpen}
variant="contained"
startIcon={<AddIcon />}
color="primary"
sx={{
borderRadius: '8px',
backgroundColor: '#043f84',
width: '97px',
height: '32px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: '20px',
padding: '7px 0',
objectFit: 'contain',
border: 'solid 1px #0f5ab2',
}}
>
</Button>
<AddWidgetPopup
label="위젯 추가"
widgetSelect={handleWidgetSelect}
useWidgetIds={useWidgetIds}
widgetOpen={widgetOpen}
/>
</>
}
>
<Box
sx={{
width: '1390px',
minWidth: '1390px',
minHeight: '1080px',
backgroundImage: `url(${bg})`,
backgroundRepeat: 'repeat',
borderRadius: '0px 0px 6px 6px',
},
}}
>
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
cols={{ lg: 12 }}
layouts={{ lg: layout }}
preventCollision={true}
containerPadding={{ lg: [24, 24] }}
margin={{ lg: [24, 24] }}
onLayoutChange={onLayoutChange}
placeholder="대시보드의 이름을 입력해 주세요"
value={dashboardTitle}
onChange={event => {
setDashboardTitle(event.target.value);
}}
/>
}
button={
<>
<Button
onClick={handleWidgetOpen}
variant="contained"
startIcon={<AddIcon />}
color="primary"
sx={{
borderRadius: '8px',
backgroundColor: '#043f84',
width: '97px',
height: '32px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: '7px 0',
objectFit: 'contain',
border: 'solid 1px #0f5ab2',
}}
>
{generateWidget()}
</ResponsiveGridLayout>
<RecommendDashboardPopup recommendOpen={recommendOpen} handleComplete={handleCompleteRecommend} />
</Box>
</DashboardTitleBox>
</PageTitleBox>
</PageContainer>
</Button>
<AddWidgetPopup
label="위젯 추가"
widgetSelect={handleWidgetSelect}
useWidgetIds={useWidgetIds}
widgetOpen={widgetOpen}
/>
</>
}
>
<Box
sx={{
width: '1390px',
minWidth: '1390px',
minHeight: '1080px',
backgroundImage: `url(${bg})`,
backgroundRepeat: 'repeat',
borderRadius: '0px 0px 6px 6px',
}}
>
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
cols={{ lg: 12 }}
layouts={{ lg: layout }}
preventCollision={true}
containerPadding={{ lg: [24, 24] }}
margin={{ lg: [24, 24] }}
onLayoutChange={onLayoutChange}
>
{generateWidget()}
</ResponsiveGridLayout>
<RecommendDashboardPopup recommendOpen={recommendOpen} handleComplete={handleCompleteRecommend} />
</Box>
</PageViewBox>
</PageTitleBox>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Card, Stack, Typography } from '@mui/material';
import { Box, Card, Hidden, useMediaQuery, useTheme } from '@mui/material';
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
import PageTitleBox from '@/components/PageTitleBox';
import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
@@ -9,7 +9,7 @@ import '/node_modules/react-grid-layout/css/styles.css';
import '/node_modules/react-resizable/css/styles.css';
import DashboardService from '@/api/dashboardService';
import { STATUS } from '@/constant';
import DashboardTitleBox from '../Components/DashboardTitleBox';
import PageViewBox from '../../../components/PageViewBox';
import ModifyButton from '@/components/button/ModifyButton';
import DeleteButton from '@/components/button/DeleteButton';
import ReloadButton from '@/components/button/ReloadButton';
@@ -30,6 +30,8 @@ const DashboardView = () => {
const snackbar = useAlert(SnackbarContext);
const { userState } = useContext(AuthContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const [dashboardInfo, setDashboardInfo] = useState({
title: '',
widgets: [],
@@ -88,7 +90,7 @@ const DashboardView = () => {
// widget 생성
const generateWidget = () => {
return dashboardInfo.widgets.map(item => {
return dashboardInfo.widgets.map((item, index) => {
return (
<Card
key={item.id}
@@ -101,36 +103,43 @@ const DashboardView = () => {
backgroundColor: '#fff',
}}
>
<WidgetWrapper widgetOption={item} dataSetId={item.datasetId} />
<WidgetWrapper widgetOption={item} dataSetId={item.datasetId} size={dashboardInfo.layout[index].w} />
</Card>
);
});
};
const handleDeleteSelect = () => {
alert.success(dashboardInfo.title + '\n대시보드를 삭제하시겠습니까?', {
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
showLoading();
DashboardService.deleteDashboard(dashboardId)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
navigate('/dashboard', { replace: true });
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
alert.success(
<Box sx={{ span: { fontWeight: 600 } }}>
<span>{dashboardInfo.title}</span>
<br />
?
</Box>,
{
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
showLoading();
DashboardService.deleteDashboard(dashboardId)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
navigate('/dashboard', { replace: true });
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
},
},
},
],
});
],
},
);
};
const handleShareToggle = () => {
@@ -183,105 +192,73 @@ const DashboardView = () => {
upperTitle="대시보드"
upperTitleLink="/dashboard"
title="대시보드 조회"
sx={{ width: '100%', marginTop: '22px' }}
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
>
<Seo title={dashboardInfo.title} />
<DashboardTitleBox
title={
<Typography
variant="subtitle1"
component="span"
<>
<Seo title={dashboardInfo.title} />
<PageViewBox
title={dashboardInfo.title}
date={dateData(dashboardInfo.updatedAt)}
button={
<>
<Hidden smDown>
<ReloadButton
size="medium"
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleRefreshClick();
}}
/>
<ModifyButton
size="medium"
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
component={RouterLink}
to={`/dashboard/modify?id=${dashboardId}&name=${dashboardInfo.title}`}
/>
<DeleteButton
size="medium"
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDeleteSelect();
}}
/>
</Hidden>
<ShareButton
handleShareToggle={handleShareToggle}
isShareOn={isShareOn}
shareId={dashboardInfo.uuid}
shareLimitDate={shareLimitDate}
setShareLimitDate={setShareLimitDate}
/>
</>
}
>
<Box
sx={{
fontWeight: 500,
paddingLeft: '18px',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '18px',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 0.89,
letterSpacing: '-0.18px',
textAlign: 'left',
color: '#141414',
flex: '1 1 auto',
minHeight: { sm: '1080px' },
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
{dashboardInfo.title}
</Typography>
}
button={
<Stack direction="row" alignItems="center" sx={{ marginRight: '20px' }}>
<span
style={{
marginRight: '36px',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333333',
}}
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
breakpoints={{ xs: 0, md: 800, lg: 1000 }}
cols={{ xs: 2, md: 8, lg: 12 }}
layouts={{ xs: layout, md: layout, lg: layout }}
containerPadding={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
margin={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
>
{dateData(dashboardInfo.updatedAt)}
</span>
<ReloadButton
size="medium"
sx={{ marginRight: '24px', padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleRefreshClick();
}}
/>
<ModifyButton
size="medium"
sx={{ marginRight: '24px', padding: 0 }}
component={RouterLink}
to={`/dashboard/modify?id=${dashboardId}&name=${dashboardInfo.title}`}
/>
<DeleteButton
size="medium"
sx={{ marginRight: '24px', padding: 0 }}
onClick={event => {
event.preventDefault();
event.stopPropagation();
handleDeleteSelect();
}}
/>
<ShareButton
handleSubmit={handleShareToggle}
isShareOn={isShareOn}
shareId={dashboardInfo.uuid}
shareLimitDate={shareLimitDate}
setShareLimitDate={setShareLimitDate}
/>
</Stack>
}
>
<Box
sx={{
width: '1390px',
minWidth: '1390px',
minHeight: '1080px',
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
cols={{ lg: 12 }}
layouts={{ lg: layout }}
containerPadding={{ lg: [24, 24] }}
margin={{ lg: [24, 24] }}
>
{generateWidget()}
</ResponsiveGridLayout>
</Box>
</DashboardTitleBox>
{generateWidget()}
</ResponsiveGridLayout>
</Box>
</PageViewBox>
</>
</PageTitleBox>
);
};

View File

@@ -1,11 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import BoardList from '@/components/BoardList';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { MenuButton } from '@/components/button/AddIconButton';
import AddIcon from '@mui/icons-material/Add';
import { Box, Stack } from '@mui/material';
import { Box, Stack, useMediaQuery, useTheme } from '@mui/material';
import DashboardService from '@/api/dashboardService';
import { STATUS } from '@/constant';
import { useAlert } from 'react-alert';
@@ -24,10 +23,12 @@ function Dashboard() {
const [loadedDashboardData, setLoadedDashboardData] = useState([]);
const [noData, setNoData] = useState(false);
const { showLoading, hideLoading } = useContext(LoadingContext);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const GTSpan = styled('span')({
fontFamily: 'Pretendard',
fontSize: '13px',
fontSize: matches ? '13px' : '10px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
@@ -63,31 +64,37 @@ function Dashboard() {
});
};
const handleDeleteSelect = item => {
console.log(item);
alert.success(item.title + '\n대시보드를 삭제하시겠습니까?', {
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
showLoading();
DashboardService.deleteDashboard(item.id)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
getDashboardList();
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
const handleDeleteSelect = (id, title) => {
alert.success(
<Box sx={{ span: { fontWeight: 600 } }}>
<span>{title}</span>
<br />
?
</Box>,
{
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
showLoading();
DashboardService.deleteDashboard(id)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
getDashboardList();
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
},
},
},
],
});
],
},
);
};
const handleMenuSelect = item => {
@@ -108,18 +115,18 @@ function Dashboard() {
<Stack
flexDirection="row"
justifyContent="space-between"
sx={{ paddingLeft: '20px', paddingRight: '217px', marginBottom: '11px', marginTop: '36px' }}
sx={{ paddingLeft: '20px', paddingRight: { xs: '44px', sm: '217px' }, marginBottom: '11px', marginTop: '36px' }}
>
<GTSpan></GTSpan>
<GTSpan></GTSpan>
</Stack>
<Box
sx={{
height: '57px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexGrow: '0',
py: '18px',
margin: '0 0 0 0',
borderRadius: '6px',
border: 'solid 1px #ddd',
@@ -128,20 +135,20 @@ function Dashboard() {
>
<span
style={{
height: '16px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '16px',
fontSize: matches ? '16px' : '14px',
fontWeight: 600,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1,
lineHeight: 1.43,
letterSpacing: 'normal',
textAlign: 'center',
color: '#333333',
}}
>
. .
.
{matches ? ' ' : <br />}
.
</span>
</Box>
</>
@@ -149,14 +156,13 @@ function Dashboard() {
};
return (
<PageContainer>
<Stack sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
<Seo title={title} />
{!dashboardId ? (
<>
<PageTitleBox
title={title}
sx={{ width: '100%' }}
button={
<MenuButton
menuList={menuList}
@@ -179,7 +185,7 @@ function Dashboard() {
) : (
<Outlet />
)}
</PageContainer>
</Stack>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Stack, Typography } from '@mui/material';
import { Stack, Typography } from '@mui/material';
import DatabaseService from '@/api/databaseService';
import { STATUS } from '@/constant';
import { useAlert } from 'react-alert';
@@ -10,33 +10,63 @@ import AddButton from '@/components/button/AddButton';
import { Link as RouterLink } from 'react-router-dom';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
import { Loading } from '@/components/loading';
import ModalPopup from '@/components/ModalPopup';
import { createColumns } from '@/utils/util';
import DataGrid, { DataGridWrapper } from '@/components/datagrid';
// import { cancelAllRequests } from '@/helpers/apiHelper';
export interface DatabaseProps {
id: number | string | null;
name: string | null;
createdAt?: string;
description?: string;
engine?: string;
timezone?: string;
type?: string;
updatedAt?: string;
}
export interface DataSetProps {
id: number;
databaseId: number;
datasetType: 'DATASET';
title: string;
query: string;
createdAt: string;
updatedAt: string;
}
export interface DataTableProps {
id: string;
tableName: string;
databaseId: number;
datasetType: 'TABLE';
}
const DataLayout = props => {
const { isViewMode, setDataSet } = props;
const [databaseList, setDatabaseList] = useState([]);
const [datasetList, setDatasetList] = useState([]);
const [tableList, setTableList] = useState([]);
const [databaseList, setDatabaseList] = useState<DatabaseProps[] | []>([]);
const [datasetList, setDatasetList] = useState<DataSetProps[] | []>([]);
const [tableList, setTableList] = useState<DataTableProps[] | []>([]);
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
const [selectedDatabase, setSelectedDatabase] = useState({
databaseId: null,
});
const [selectedDataset, setSelectedDataset] = useState(null);
const handleSelectDatabase = enteredData => {
return setSelectedDatabase(prevState => ({ ...prevState, ...enteredData }));
};
const { loading, showLoading, hideLoading } = useContext(LoadingContext);
const [selectedDatabase, setSelectedDatabase] = useState<DatabaseProps>({ id: null, name: null });
const [selectedDataset, setSelectedDataset] = useState<DataSetProps | DataTableProps | null>(null);
const [open, setOpen] = useState(false);
const [gridData, setGridData] = useState<any[]>([]);
const [gridColumns, setGridColumns] = useState<any[]>([]);
useEffect(() => {
getDatabaseList();
}, []);
useEffect(() => {
if (selectedDatabase.databaseId) getDatabaseInfo();
}, [selectedDatabase.databaseId]);
if (selectedDatabase.id) {
getDatabaseInfo(selectedDatabase.id);
}
}, [selectedDatabase.id]);
/**
* 데이터베이스 목록조회
@@ -45,9 +75,11 @@ const DataLayout = props => {
showLoading();
DatabaseService.selectDatabaseList()
.then(response => {
setDatabaseList(response.data.data);
if (response.data.data.length > 0) {
setSelectedDatabase({ databaseId: response.data.data[0].id });
const resData = response.data.data;
setDatabaseList(resData);
if (resData.length > 0) {
const [firstItem] = resData;
setSelectedDatabase(firstItem);
}
})
.finally(() => {
@@ -55,9 +87,9 @@ const DataLayout = props => {
});
};
const getDatabaseInfo = () => {
const getDatabaseInfo = databaseId => {
showLoading();
DatabaseService.selectDatabase(selectedDatabase.databaseId)
DatabaseService.selectDatabase(databaseId)
.then(response => {
if (response.data.status === 'SUCCESS') {
setDatasetList(response.data.data.datasets);
@@ -78,16 +110,51 @@ const DataLayout = props => {
});
};
const removeDatabase = (id, name) => {
console.log('removeDatabase', id);
alert.success(`${name}\n데이터베이스를 삭제하시겠습니까?`, {
const getData = selectedData => {
let param;
switch (selectedData?.datasetType) {
case 'DATASET':
const { id, ...rest } = selectedData;
param = { ...rest, datasetId: id };
break;
case 'TABLE':
param = { ...selectedData };
break;
default:
return;
}
showLoading();
DatabaseService.selectData(param)
.then(response => {
if (response.data.status === STATUS.SUCCESS) {
setGridData(response.data.data.datas);
setGridColumns(createColumns(response.data.data.datas));
}
})
.catch(error => {
console.log('error', error);
setGridData([]);
setGridColumns([]);
})
.finally(() => {
hideLoading();
});
};
const handleDatabaseClick = (item: DatabaseProps) => {
setSelectedDatabase(item);
};
const handleDatabaseRemove = item => {
console.log('handleDatabaseRemove', item);
alert.success(`${item.name}\n데이터베이스를 삭제하시겠습니까?`, {
title: '데이터베이스 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
DatabaseService.deleteDatabase(id).then(response => {
DatabaseService.deleteDatabase(item.databaseId).then(response => {
if (response.data.status === STATUS.SUCCESS) {
getDatabaseList();
snackbar.success('데이터베이스가 삭제되었습니다.');
@@ -99,14 +166,29 @@ const DataLayout = props => {
});
};
const handleSelectDataset = item => {
console.log('handleSelectDataset', item);
if (setDataSet) setDataSet(item);
const handleDataSetClick = (item: DataTableProps | DataSetProps) => {
console.log('selected Data', item);
if (isViewMode) {
setDataSet(item);
} else {
setOpen(true);
getData(item);
}
setSelectedDataset(item);
};
const handleDeleteDataset = item => {
console.log('handleDeleteDataset', item);
const handleClose = () => {
setOpen(false);
setGridData([]);
setGridColumns([]);
// if (loading) {
// // 진행되고 있는 모든 요청 취소
// cancelAllRequests();
// }
};
const handleDataSetRemove = item => {
console.log('handleDataSetRemove', item);
alert.success(`${item.title}\n데이터셋을 삭제하시겠습니까?`, {
title: '데이터베이스 삭제',
closeCopy: '취소',
@@ -115,7 +197,7 @@ const DataLayout = props => {
copy: '삭제',
onClick: () => {
DatasetService.deleteDataset(item.id).then(() => {
getDatabaseInfo();
getDatabaseInfo(item.id);
snackbar.success('데이터셋이 삭제되었습니다.');
});
},
@@ -125,9 +207,13 @@ const DataLayout = props => {
};
return (
<Stack sx={{ width: '100%' }} direction="row">
<Stack sx={{ width: { xs: '270px', md: '404px' }, px: '24px', pt: '30px' }}>
<Stack direction="row" sx={{ mb: '12px' }}>
<Stack direction={{ xs: 'column', sm: 'row' }} flex="1 1 auto" sx={{ width: '100%' }}>
<Stack
direction="column"
flex="1 1 auto"
sx={{ width: { xs: '100%', md: '404px' }, height: '100%', px: '24px', pt: '30px' }}
>
<Stack direction="row">
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
</Typography>
@@ -135,59 +221,73 @@ const DataLayout = props => {
</Stack>
<DatabaseCardList
data={databaseList}
selectedDatabase={selectedDatabase}
disabledIcons={!!isViewMode}
onUpdate={handleSelectDatabase}
onRemove={removeDatabase}
minWidth="100%"
selectedData={selectedDatabase}
isViewMode={isViewMode}
handleDataClick={handleDatabaseClick}
handleDataRemove={handleDatabaseRemove}
/>
</Stack>
<Stack sx={{ width: { xs: 'calc(100% - 270px)', md: 'calc(100% - 404px)' }, backgroundColor: '#f5f6f8' }}>
<Stack sx={{ width: '100%', px: '24px', pt: '30px' }}>
<Stack direction="row" sx={{ mb: '12px' }}>
<Stack
direction="column"
sx={{ flex: '1 1 auto', width: { xs: '100%', md: 'calc(100% - 404px)' }, backgroundColor: '#f5f6f8' }}
>
<Stack direction="column" sx={{ width: '100%', px: '24px', pt: '30px' }}>
<Stack direction="row">
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
</Typography>
{isViewMode ? (
<></>
) : (
<AddButton component={RouterLink} to={`set/create/${selectedDatabase.databaseId}`} sx={{ ml: '14px' }} />
<AddButton component={RouterLink} to={`set/create/${selectedDatabase.id}`} sx={{ ml: '14px' }} />
)}
</Stack>
{datasetList.length > 0 ? (
<DatasetCardList
data={datasetList}
selectedDataset={selectedDataset}
onSelectDataset={handleSelectDataset}
onDeleteDataset={handleDeleteDataset}
disabledIcons={!!isViewMode}
/>
) : (
<Box sx={{ height: '100px' }} />
)}
<DatasetCardList
isViewMode={isViewMode}
data={datasetList}
selectedData={selectedDataset}
handleDataClick={handleDataSetClick}
handleDataRemove={handleDataSetRemove}
/>
</Stack>
<Stack sx={{ width: '100%', px: '24px', pt: '30px' }}>
<Stack direction="row" sx={{ mb: '12px' }}>
<Stack direction="column" sx={{ flex: '1 1 auto', width: '100%', minHeight: '50%', px: '24px', pt: '30px' }}>
<Stack direction="row">
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
</Typography>
</Stack>
<DatasetCardList
isViewMode={isViewMode}
data={tableList}
selectedDataset={selectedDataset}
onSelectDataset={handleSelectDataset}
disabledIcons={true}
selectedData={selectedDataset}
handleDataClick={handleDataSetClick}
/>
<ModalPopup
open={open}
handleClose={handleClose}
title={selectedDataset && (selectedDataset?.['tableName'] || selectedDataset?.['title'])}
>
{loading ? (
<Loading in={loading} style={{ position: 'static', backgroundColor: 'transparent' }} />
) : (
<DataGridWrapper>
<DataGrid
minBodyHeight={100}
bodyHeight={'fitToParent'}
data={gridData}
columns={gridColumns}
columnOptions={{
resizable: true,
}}
/>
</DataGridWrapper>
)}
</ModalPopup>
</Stack>
</Stack>
</Stack>
);
};
DataLayout.defaultProps = {
data: {},
naviUrl: {},
};
export default DataLayout;

View File

@@ -17,6 +17,7 @@ import { STATUS } from '@/constant';
import { getDatabaseIcon } from '@/widget/utils/iconUtil';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
import { createColumns } from '@/utils/util';
const DataSet = () => {
const { setId, sourceId } = useParams();
@@ -61,22 +62,6 @@ const DataSet = () => {
if (!databaseId) getDatabaseId();
}, [databaseList, datasetInfo]);
/**
* 데이터 그리드 컬럼 생성
* @param data
*/
const createColumns = data => {
let target = null;
if (data instanceof Array && data.length > 0) {
target = data[0];
} else if (data instanceof Object) {
target = data;
}
return Object.keys(target).map(key => {
return { name: key, header: key, align: key, width: 200, sortable: true };
});
};
const addCompleter = () => {
const rhymeCompleter = {
getCompletions: (editor, session, pos, prefix, callback) => {
@@ -305,9 +290,9 @@ const DataSet = () => {
>
<Select
id="databaseId"
sx={{ width: '500px' }}
sx={{ maxWidth: '500px' }}
displayEmpty
disabled={isModifyMode}
disabled={isModifyMode || !databaseList.length}
size="small"
value={databaseId ?? ''}
// renderValue={selected => {
@@ -320,11 +305,15 @@ const DataSet = () => {
// }}
onChange={onChangeDatabaseId}
>
{databaseList.map(item => (
<MenuItem key={item.id} value={item.id ?? ''}>
{item.name}
</MenuItem>
))}
{databaseList.length ? (
databaseList.map(item => (
<MenuItem key={item.id} value={item.id ?? ''}>
{item.name}
</MenuItem>
))
) : (
<MenuItem value=""> .</MenuItem>
)}
</Select>
<TextField

View File

@@ -1,6 +1,5 @@
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
import { Stack, Typography } from '@mui/material';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import ImgCardList from '@/components/ImgCardList';
import ConfirmCancelButton from '@/components/button/ConfirmCancelButton';
@@ -292,48 +291,46 @@ function DataSource() {
};
return (
<PageContainer>
<PageTitleBox
upperTitle="데이터"
upperTitleLink="/data"
title={'데이터 소스 연결'}
button={
<ConfirmCancelButton
confirmProps={{ disabled: !isConnected, onClick: handleSaveClick }}
cancelProps={{ onClick: handleCancelClick }}
<PageTitleBox
upperTitle="데이터"
upperTitleLink="/data"
title={'데이터 소스 연결'}
button={
<ConfirmCancelButton
confirmProps={{ disabled: !isConnected, onClick: handleSaveClick }}
cancelProps={{ onClick: handleCancelClick }}
/>
}
sx={{ p: 0 }}
>
<Stack sx={{ width: '100%' }}>
<Stack sx={{ p: '30px 25px 50px 25px' }}>
<Typography
variant="subtitle1"
component="span"
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
>
step.01
</Typography>
<ImgCardList
data={typeList}
selectedType={dataType}
setSelectedType={setDataType}
handleTypeClick={handleTypeClick}
/>
}
sx={{ p: 0 }}
>
<Stack sx={{ width: '100%' }}>
<Stack sx={{ p: '30px 25px 50px 25px' }}>
<Typography
variant="subtitle1"
component="span"
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
>
step.01
</Typography>
<ImgCardList
data={typeList}
selectedType={dataType}
setSelectedType={setDataType}
handleTypeClick={handleTypeClick}
/>
</Stack>
<Stack sx={{ p: '30px 25px 50px 25px', bgcolor: '#f5f6f8' }}>
<Typography
variant="subtitle1"
component="span"
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
>
step.02
</Typography>
{dbType()}
</Stack>
</Stack>
</PageTitleBox>
</PageContainer>
<Stack sx={{ p: '30px 25px 50px 25px', bgcolor: '#f5f6f8' }}>
<Typography
variant="subtitle1"
component="span"
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
>
step.02
</Typography>
{dbType()}
</Stack>
</Stack>
</PageTitleBox>
);
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import PageContainer from '@/components/PageContainer';
import DataLayout from './DataLayout';
import PageTitleBox from '@/components/PageTitleBox';
import Seo from '@/seo/Seo';
@@ -13,13 +12,10 @@ const title = '데이터';
const Data = () => {
return (
<PageContainer>
<PageTitleBox title={title} sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}>
<Seo title={title} />
<PageTitleBox title={title} sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}>
<DataLayout />
</PageTitleBox>
</PageContainer>
<DataLayout />
</PageTitleBox>
);
};

View File

@@ -5,7 +5,7 @@ import { useAlert } from 'react-alert';
import { LoadingContext } from '@/contexts/LoadingContext';
import { ReactComponent as Logo } from '@/assets/images/logo.svg';
import backgroundImage from '@/assets/images/visual-bg.png';
import Copyright from '@/components/Copyright';
import { Copyright } from '@/layouts/Footer';
import authService from '@/api/authService';
import { checkId, checkPwd } from '@/utils/util';
import { SnackbarContext } from '@/contexts/AlertContext';
@@ -119,14 +119,21 @@ const Login = () => {
<RouterLink to="/">
<Logo width="223px" height="43px" />
</RouterLink>
<Typography sx={{ mt: '17px', fontSize: '16px', color: '#043f84' }}>
<Typography sx={{ mt: '17px', fontSize: '16px', color: '#043f84', textAlign: 'center' }}>
{' '}
<Typography component="span" sx={{ fontSize: '16px', fontWeight: 'bold' }}>
</Typography>
</Typography>
<Stack component="form" onSubmit={handleLogin} noValidate sx={{ width: '360px', mt: '56px' }} spacing="20px">
<Stack
component="form"
onSubmit={handleLogin}
noValidate
sx={{ width: { xs: 'calc(100% - 40px)', sm: '360px' }, mt: '56px' }}
spacing="20px"
>
<TextField
autoFocus={true}
label="User ID"
name="userId"
value={userInfo.userId}
@@ -134,6 +141,11 @@ const Login = () => {
margin="normal"
required={true}
fullWidth
sx={{ height: { xs: '44px', sm: '36px' } }}
InputLabelProps={{ sx: { pt: { xs: '6px', sm: 0 } } }}
InputProps={{
sx: { height: { xs: '44px', sm: '36px' }, input: { padding: '12px 14px' } },
}}
/>
<TextField
label="Password"
@@ -144,9 +156,19 @@ const Login = () => {
margin="normal"
required={true}
fullWidth
sx={{ height: '36px' }}
sx={{ height: { xs: '44px', sm: '36px' } }}
InputLabelProps={{ sx: { pt: { xs: '6px', sm: 0 } } }}
InputProps={{
sx: { height: { xs: '44px', sm: '36px' }, input: { padding: '12px 14px' } },
}}
/>
<Button type="submit" size="large" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
<Button
type="submit"
size="large"
fullWidth
variant="contained"
sx={{ height: { xs: '50px', sm: '44px' }, mt: 3, mb: 2 }}
>
Login
</Button>
</Stack>

View File

@@ -1,16 +1,14 @@
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Box, Card, Stack, Typography } from '@mui/material';
import { Box, Card, Typography } from '@mui/material';
import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
import { useAlert } from 'react-alert';
import { Responsive, WidthProvider } from 'react-grid-layout';
import '/node_modules/react-grid-layout/css/styles.css';
import '/node_modules/react-resizable/css/styles.css';
import DashboardTitleBox from '../Dashboard/Components/DashboardTitleBox';
import PageViewBox from '../../components/PageViewBox';
import { LoadingContext } from '@/contexts/LoadingContext';
import shareService from '@/api/shareService';
import { LandingLogo } from '@/layouts/Header/Logo';
import Copyright from '@/components/Copyright';
import Seo from '@/seo/Seo';
import { dateData } from '@/utils/util';
import { setShareToken } from '@/helpers/shareHelper';
@@ -20,7 +18,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
const Share = () => {
const { dashboardUuid } = useParams();
const alert = useAlert();
const { showLoading, hideLoading } = useContext(LoadingContext);
const { loading, showLoading, hideLoading } = useContext(LoadingContext);
const [dashboardInfo, setDashboardInfo] = useState({
title: '',
widgets: [],
@@ -109,124 +107,67 @@ const Share = () => {
// 데이터가 없는 상태별 텍스트
const generateInvalidText = () => {
if (!isInvalidData) {
return <> .</>;
if (loading) {
return '데이터를 불러오고 있습니다.';
} else if (isInvalidData === 401) {
return (
<>
.
<br />
.
</>
);
return '대시보드의 공유 기간이 만료되었습니다.\n대시보드의 공유 상태를 다시 확인해 주세요.';
} else {
return (
<>
.
<br />
URL을 .
</>
);
return '대시보드가 존재하지 않습니다.\n대시보드의 URL을 다시 확인해 주세요.';
}
};
return (
<Stack sx={{ width: '1392px', m: 'auto', mt: '32px' }}>
<LandingLogo sx={{ mb: '5px' }} />
{!isShareOn ? (
// 공유 URL이 유효하지 않을 때 보여줄 화면
<DashboardTitleBox>
<Box
sx={{
width: '1390px',
minWidth: '1390px',
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
<Typography
sx={{
margin: '200px auto',
fontSize: '16px',
fontWeight: 600,
textAlign: 'center',
lineHeight: '1.6',
color: '#333',
}}
>
{generateInvalidText()}
</Typography>
</Box>
</DashboardTitleBox>
) : (
<DashboardTitleBox
title={
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: 500,
paddingLeft: '18px',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '18px',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 0.89,
letterSpacing: '-0.18px',
textAlign: 'left',
color: '#141414',
}}
>
{dashboardInfo.title}
</Typography>
}
button={
<Stack direction="row" alignItems="center" sx={{ marginRight: '24px' }}>
<span
style={{
height: '16px',
fontFamily: 'Pretendard',
fontSize: '14px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333333',
}}
>
{`편집일 : ${dateData(dashboardInfo.updatedAt)}`}
</span>
</Stack>
}
return !isShareOn ? (
<PageViewBox sx={{ borderTop: '1px solid #ddd' }}>
<Box
sx={{
flex: '1 1 auto',
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
<Typography
sx={{
margin: '200px auto',
fontSize: '16px',
fontWeight: 600,
textAlign: 'center',
lineHeight: '1.6',
whiteSpace: 'pre-wrap',
color: '#333',
}}
>
<Seo title={dashboardInfo.title} />
<Box
sx={{
width: '1390px',
minWidth: '1390px',
minHeight: '1080px',
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
cols={{ lg: 12 }}
layouts={{ lg: layout }}
containerPadding={{ lg: [24, 24] }}
margin={{ lg: [24, 24] }}
>
{generateWidget()}
</ResponsiveGridLayout>
</Box>
</DashboardTitleBox>
)}
<Copyright sx={{ mt: '40px', mb: '75px' }} />
</Stack>
{generateInvalidText()}
</Typography>
</Box>
</PageViewBox>
) : (
<PageViewBox
title={dashboardInfo.title}
date={`${dateData(dashboardInfo.updatedAt)}`}
sx={{ borderTop: '1px solid #ddd' }}
>
<Seo title={dashboardInfo.title} />
<Box
sx={{
flex: '1 1 auto',
minHeight: { sm: '1080px' },
backgroundColor: '#f9f9fa',
borderRadius: '0px 0px 6px 6px',
}}
>
<ResponsiveGridLayout
rowHeight={88}
compactType={null}
breakpoints={{ xs: 0, md: 800, lg: 1000 }}
cols={{ xs: 2, md: 8, lg: 12 }}
layouts={{ xs: layout, md: layout, lg: layout }}
containerPadding={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
margin={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
>
{generateWidget()}
</ResponsiveGridLayout>
</Box>
</PageViewBox>
);
};

View File

@@ -4,7 +4,7 @@ import { ReactComponent as Logo } from '@/assets/images/logo.svg';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { LoadingContext } from '@/contexts/LoadingContext';
import { useAlert } from 'react-alert';
import Copyright from '@/components/Copyright';
import { Copyright } from '@/layouts/Footer';
import authService from '@/api/authService';
import { SnackbarContext } from '@/contexts/AlertContext';
import { checkEmail, checkId, checkPwd } from '@/utils/util';

View File

@@ -1,8 +1,13 @@
import React from 'react';
import { Box } from '@mui/material';
import PageTitleBox from '@/components/PageTitleBox';
function Status404(props) {
return <Box p={3}>404 Error</Box>;
function Status404() {
return (
<PageTitleBox title="404 Error">
<Box sx={{ m: 'auto', fontSize: '18px', fontWeight: 600 }}> .</Box>
</PageTitleBox>
);
}
export default Status404;

View File

@@ -45,10 +45,10 @@ const WidgetAttributeSelect = props => {
const getData = () => {
const param = isModifyMode
? { datasetType: widgetOption.datasetType, datasetId: widgetOption.datasetId }
? { ...widgetOption }
: dataset.datasetType === 'TABLE'
? { databaseId: dataset.databaseId, datasetType: dataset.datasetType, tableName: dataset.tableName }
: { databaseId: dataset.databaseId, datasetType: dataset.datasetType, datasetId: dataset.id };
? { ...dataset }
: { ...dataset, datasetId: dataset.id };
console.log('getData param', param);
showLoading();
DatabaseService.selectData(param)

View File

@@ -8,6 +8,7 @@ function WidgetTypeSelect(props) {
return (
<Box
sx={{
flex: '1 1 auto',
height: '100%',
px: '25px',
pt: '22px',

View File

@@ -1,7 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Stack, Step, StepLabel, Stepper, SvgIcon } from '@mui/material';
import PageTitleBox from '@/components/PageTitleBox';
import PageContainer from '@/components/PageContainer';
import WidgetDataSelect from './WidgetDataSelect';
import WidgetTypeSelect from './WidgetTypeSelect';
import WidgetAttributeSelect from './WidgetAttributeSelect';
@@ -110,113 +109,117 @@ const WidgetCreate = () => {
};
return (
<PageContainer>
<PageTitleBox
fixed
title={title}
upperTitle="위젯"
upperTitleLink="/widget"
sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}
button={
<Stack direction="row" gap="10px">
<PageTitleBox
fixed
title={title}
upperTitle="위젯"
upperTitleLink="/widget"
sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}
button={
<Stack direction="row" gap="10px">
<Button
variant="outlined"
onClick={handleBack}
disabled={activeStep === 0}
startIcon={
<SvgIcon
component={LeftArrow}
sx={{
width: '14px',
height: '14px',
padding: '1px',
}}
inheritViewBox
/>
}
sx={{
color: '#043f84',
'&.Mui-disabled &.MuiButton-startIcon': {
color: '#fff',
},
}}
>
</Button>
{activeStep !== 2 ? (
<Button
variant="outlined"
onClick={handleBack}
disabled={activeStep === 0}
startIcon={
variant="contained"
type="button"
onClick={handleNext}
disabled={isNextButtonDisabled}
endIcon={
<SvgIcon
component={LeftArrow}
sx={{
width: '14px',
height: '14px',
padding: '1px',
}}
sx={{ width: '14px', height: '14px', transform: 'rotate(180deg)', padding: '1px' }}
inheritViewBox
/>
}
sx={{
color: '#043f84',
'&.Mui-disabled &.MuiButton-startIcon': {
color: '#fff',
},
}}
sx={{ backgroundColor: '#043f84' }}
>
</Button>
{activeStep !== 2 ? (
<Button
variant="contained"
type="button"
onClick={handleNext}
disabled={isNextButtonDisabled}
endIcon={
<SvgIcon
component={LeftArrow}
sx={{ width: '14px', height: '14px', transform: 'rotate(180deg)', padding: '1px' }}
inheritViewBox
/>
}
sx={{ backgroundColor: '#043f84' }}
>
</Button>
) : (
' '
)}
</Stack>
}
) : (
' '
)}
</Stack>
}
>
<Box
sx={{
position: 'fixed',
zIndex: 1000,
width: '100%',
mt: '56px',
borderBottom: '1px solid #e3e7ea',
backgroundColor: '#fff',
}}
>
<Box
<Stepper
activeStep={activeStep}
sx={{
position: 'fixed',
zIndex: 1000,
width: '100%',
mt: '56px',
borderBottom: '1px solid #e3e7ea',
backgroundColor: '#fff',
width: '50%',
minWidth: '450px',
maxWidth: '564px',
height: '72px',
m: 'auto',
}}
>
<Stepper
activeStep={activeStep}
sx={{
width: '50%',
minWidth: '450px',
maxWidth: '564px',
height: '72px',
m: 'auto',
}}
>
{steps.map(label => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</Box>
<Box mt="129px">
{activeStep === 0 ? (
<WidgetDataSelect setDataSet={setDataset} />
) : activeStep === 1 ? (
<WidgetTypeSelect widgetType={widgetOption} setWidgetType={setWidgetOption} componentList={componentList} />
) : (
<WidgetAttributeSelect
dataset={dataset}
widgetOption={widgetOption}
saveWidgetInfo={saveWidgetInfo}
widgetTypeName={widgetOption.title}
widgetTypeDescription={widgetOption.description}
/>
)}
</Box>
</PageTitleBox>
</PageContainer>
{steps.map(label => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</Box>
<Box
sx={{
display: 'flex',
flex: '1 1 auto',
mt: '129px',
}}
>
{activeStep === 0 ? (
<WidgetDataSelect setDataSet={setDataset} />
) : activeStep === 1 ? (
<WidgetTypeSelect widgetType={widgetOption} setWidgetType={setWidgetOption} componentList={componentList} />
) : (
<WidgetAttributeSelect
dataset={dataset}
widgetOption={widgetOption}
saveWidgetInfo={saveWidgetInfo}
widgetTypeName={widgetOption.title}
widgetTypeDescription={widgetOption.description}
/>
)}
</Box>
</PageTitleBox>
);
};

View File

@@ -1,5 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import { ConfirmButton } from '@/components/button/ConfirmCancelButton';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
@@ -82,33 +81,31 @@ const WidgetModify = () => {
};
return (
<PageContainer>
<PageTitleBox
upperTitle="위젯"
upperTitleLink="/widget"
title="위젯 편집"
sx={{ padding: 0 }}
button={
<ConfirmButton
confirmLabel="저장"
confirmProps={{
form: 'widgetAttribute',
type: 'submit',
variant: 'contained',
}}
/>
}
>
<WidgetAttributeSelect
widgetTypeName={widgetInfo.componentTitle}
widgetTypeDescription={widgetInfo.componentDescription}
isModifyMode={true}
dataSetId={widgetInfo.datasetId}
widgetOption={widgetInfo}
saveWidgetInfo={saveWidgetInfo}
<PageTitleBox
upperTitle="위젯"
upperTitleLink="/widget"
title="위젯 편집"
sx={{ padding: 0 }}
button={
<ConfirmButton
confirmLabel="저장"
confirmProps={{
form: 'widgetAttribute',
type: 'submit',
variant: 'contained',
}}
/>
</PageTitleBox>
</PageContainer>
}
>
<WidgetAttributeSelect
widgetTypeName={widgetInfo.componentTitle}
widgetTypeDescription={widgetInfo.componentDescription}
isModifyMode={true}
dataSetId={widgetInfo.datasetId}
widgetOption={widgetInfo}
saveWidgetInfo={saveWidgetInfo}
/>
</PageTitleBox>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Avatar, Card, Stack, Typography } from '@mui/material';
import { Box, Card, Hidden } from '@mui/material';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import PageTitleBox from '@/components/PageTitleBox';
import WidgetService from '@/api/widgetService';
@@ -8,7 +8,7 @@ import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
import ReloadButton from '@/components/button/ReloadButton';
import ModifyButton from '@/components/button/ModifyButton';
import DeleteButton from '@/components/button/DeleteButton';
import DashboardTitleBox from '@/pages/Dashboard/Components/DashboardTitleBox';
import PageViewBox from '@/components/PageViewBox';
import { useAlert } from 'react-alert';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
@@ -67,85 +67,53 @@ const WidgetView = () => {
};
const removeWidget = () => {
alert.success(`${widgetOption.title}\n위젯을 삭제하시겠습니까?`, {
title: '위젯 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
showLoading();
WidgetService.deleteWidget(widgetId)
.then(response => {
if (response.status === 200) {
navigate('/widget', { replace: true });
snackbar.success('위젯이 삭제되었습니다.');
} else {
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
alert.success(
<Box sx={{ span: { fontWeight: 600 } }}>
<span>{widgetOption.title}</span>
<br />
?
</Box>,
{
title: '위젯 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
showLoading();
WidgetService.deleteWidget(widgetId)
.then(response => {
if (response.status === 200) {
navigate('/widget', { replace: true });
snackbar.success('위젯이 삭제되었습니다.');
} else {
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
},
},
},
],
});
],
},
);
};
return (
<PageTitleBox upperTitle="위젯" upperTitleLink="/widget" title="위젯 조회" sx={{ width: '100%', marginTop: '22px' }}>
<PageTitleBox
upperTitle="위젯"
upperTitleLink="/widget"
title="위젯 조회"
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
>
<Seo title={widgetOption.title} />
<DashboardTitleBox
sx={{ minWidth: '600px', maxWidth: '1392px', width: '95%' }}
title={
<Stack
direction="row"
alignItems="center"
sx={{
pl: '20px',
}}
>
<Avatar
src={`/static/images/${widgetOption.icon}`}
sx={{ width: '30px', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
/>
<Typography
variant="subtitle1"
component="span"
sx={{
fontWeight: 500,
paddingLeft: '14px',
height: '16px',
fontSize: '18px',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 0.89,
letterSpacing: '-0.18px',
textAlign: 'left',
color: '#141414',
}}
>
{widgetOption.componentTitle}
</Typography>
</Stack>
}
<PageViewBox
iconName={widgetOption.icon}
title={widgetOption.componentTitle}
date={dateData(widgetOption.updatedAt)}
button={
<Stack direction="row" alignItems="center" sx={{ marginRight: '20px' }}>
<span
style={{
marginRight: '56px',
height: '16px',
fontSize: '14px',
fontWeight: '500',
lineHeight: '1.14',
letterSpacing: 'normal',
textAlign: 'left',
color: '#333',
}}
>
{dateData(widgetOption.updatedAt)}
</span>
<Hidden smDown>
<ReloadButton
size="medium"
sx={{ marginRight: '36px', padding: 0 }}
@@ -173,16 +141,16 @@ const WidgetView = () => {
removeWidget();
}}
/>
</Stack>
</Hidden>
}
>
<Card
sx={{
width: '60%',
width: { xs: 'calc(100% - 40px)', sm: '60%' },
height: '50vw',
minHeight: '300px',
maxHeight: '700px',
margin: '54px auto',
margin: { xs: '20px auto', sm: '54px auto' },
borderRadius: '8px',
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
border: 'solid 1px #e2e2e2',
@@ -191,7 +159,7 @@ const WidgetView = () => {
>
<WidgetWrapper widgetOption={widgetOption} dataSetId={widgetOption.datasetId} />
</Card>
</DashboardTitleBox>
</PageViewBox>
</PageTitleBox>
);
};

View File

@@ -1,13 +1,12 @@
import React, { useContext, useEffect, useState } from 'react';
import { Link as RouterLink, Outlet, useParams } from 'react-router-dom';
import { useAlert } from 'react-alert';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import BoardList from '@/components/BoardList';
import WidgetService from '@/api/widgetService';
import { LoadingContext } from '@/contexts/LoadingContext';
import AddIcon from '@mui/icons-material/Add';
import { Box, Button, Stack } from '@mui/material';
import { Box, Button, Stack, useMediaQuery, useTheme } from '@mui/material';
import { styled } from '@mui/system';
import { STATUS } from '@/constant';
import { SnackbarContext } from '@/contexts/AlertContext';
@@ -23,10 +22,12 @@ const Widget = () => {
const [widgetList, setWidgetList] = useState([]);
const [noData, setNoData] = useState(false);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const GTSpan = styled('span')({
fontFamily: 'Pretendard',
fontSize: '13px',
fontSize: matches ? '13px' : '10px',
fontWeight: '500',
fontStretch: 'normal',
fontStyle: 'normal',
@@ -59,31 +60,38 @@ const Widget = () => {
});
};
const removeWidget = item => {
alert.success(`${item.title}\n위젯을 삭제하시겠습니까?`, {
title: '위젯 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
showLoading();
WidgetService.deleteWidget(item.id)
.then(response => {
if (response.status === 200) {
getWidgetList();
snackbar.success('위젯이 삭제되었습니다.');
} else {
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
const removeWidget = (id, title) => {
alert.success(
<Box sx={{ span: { fontWeight: 600 } }}>
<span>{title}</span>
<br />
?
</Box>,
{
title: '위젯 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
showLoading();
WidgetService.deleteWidget(id)
.then(response => {
if (response.status === 200) {
getWidgetList();
snackbar.success('위젯이 삭제되었습니다.');
} else {
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
}
})
.finally(() => {
hideLoading();
});
},
},
},
],
});
],
},
);
};
// 목록이 없을때 보여줄 화면
@@ -93,18 +101,18 @@ const Widget = () => {
<Stack
flexDirection="row"
justifyContent="space-between"
sx={{ paddingLeft: '20px', paddingRight: '217px', marginBottom: '11px', marginTop: '36px' }}
sx={{ paddingLeft: '20px', paddingRight: { xs: '20px', sm: '217px' }, marginBottom: '11px', marginTop: '36px' }}
>
<GTSpan></GTSpan>
<GTSpan></GTSpan>
</Stack>
<Box
sx={{
height: '57px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexGrow: '0',
py: '18px',
margin: '0 0 0 0',
borderRadius: '6px',
border: 'solid 1px #ddd',
@@ -113,20 +121,21 @@ const Widget = () => {
>
<span
style={{
height: '16px',
flexGrow: 0,
fontFamily: 'Pretendard',
fontSize: '16px',
fontSize: matches ? '16px' : '14px',
fontWeight: 600,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1,
lineHeight: 1.43,
letterSpacing: 'normal',
textAlign: 'center',
color: '#333333',
}}
>
. .
.
{matches ? ' ' : <br />}
.
</span>
</Box>
</>
@@ -134,7 +143,7 @@ const Widget = () => {
};
return (
<PageContainer>
<Stack sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
<Seo title={title} />
{!widgetId ? (
@@ -163,7 +172,7 @@ const Widget = () => {
) : (
<Outlet />
)}
</PageContainer>
</Stack>
);
};

View File

@@ -17,11 +17,11 @@ import { ProtectedRoute } from '@/router/ProtectedRoute';
import Layout from '@/layouts/Layout';
import Login from '@/pages/Login';
import Share from '@/pages/Share';
import PublicLayout from '@/layouts/PublicLayout';
function Router() {
return (
<Routes>
<Route path="/share/:dashboardUuid" element={<Share />} />
<Route
path="/"
element={
@@ -58,10 +58,14 @@ function Router() {
<Route path="/data/set/modify" element={<DataSet />}>
<Route path=":setId" element={<DataSet />} />
</Route>
<Route path="/*" element={<Status404 />} />
</Route>
<Route path="/login" element={<Login />} />
{/*<Route path="/signup" element={<SignUp />} />*/}
<Route path="/*" element={<Status404 />} />
<Route path="/" element={<PublicLayout />}>
<Route path="/share/:dashboardUuid" element={<Share />} />
<Route path="*" element={<Status404 />} />
</Route>
</Routes>
);
}

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { ROUTE_URL } from '@/constant';
import { Helmet } from 'react-helmet-async';
const Seo = props => {
const {
title = 'VanillaMeta',
description = '최신 엔터프라이즈용 비즈니스 인텔리전스 웹 애플리케이션, VanillaMeta',
image = `${ROUTE_URL}/static/images/logo/vanillaMeta-og.jpg`,
url = ROUTE_URL,
image = `${process.env.PUBLIC_URL}/static/images/logo/vanillaMeta-og.jpg`,
url = process.env.PUBLIC_URL,
} = props;
const titleText = title === 'VanillaMeta' ? 'VanillaMeta' : title + ' - VanillaMeta';

View File

@@ -375,7 +375,7 @@ export default createTheme({
'html, body, #root': {
width: '100%',
height: '100%',
minWidth: '600px',
minWidth: '320px',
fontFamily: 'Pretendard',
backgroundColor: '#FFFFFF',
color: '#1F2123',

View File

@@ -28,3 +28,19 @@ export const dateData = data => {
}
return result;
};
/**
* 데이터 그리드 컬럼 생성
* @param data
*/
export const createColumns = data => {
let target = null;
if (data instanceof Array && data.length > 0) {
target = data[0];
} else if (data instanceof Object) {
target = data;
}
return Object.keys(target).map(key => {
return { name: key, header: key, minWidth: 200, sortable: true };
});
};

View File

@@ -55,9 +55,16 @@ const NumericBoard = props => {
<Typography
component="span"
sx={{
display: 'block',
maxWidth: '700px', // 글자 최대너비 제한
width: '100%',
mx: 'auto',
fontSize: componentOption.header.fontSize,
color: componentOption.header.color,
textAlign: 'center',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{componentOption.header.title}
@@ -65,10 +72,17 @@ const NumericBoard = props => {
<Typography
component="span"
sx={{
display: 'block',
maxWidth: '700px', // 글자 최대너비 제한
width: '100%',
mx: 'auto',
fontSize: componentOption.content.fontSize,
color: componentOption.content.color,
fontWeight: 'bold',
textAlign: 'center',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{score}

View File

@@ -1,23 +1,42 @@
import React, { useEffect, useState } from 'react';
import { Stack } from '@mui/material';
import DataGrid from '@/components/datagrid';
import React, { useEffect, useRef, useState } from 'react';
import DataGrid, { DataGridWrapper } from '@/components/datagrid';
import _ from 'lodash';
const TableBoard = props => {
const { option, dataSet } = props;
const [columns, setColumns] = useState([]);
const [resizeObserver, setResizeObserver] = useState(null);
const wrapperRef = useRef(null);
const defaultComponentOption = {
columns: [],
};
const [columns, setColumns] = useState([]);
useEffect(() => {
if (option && dataSet) {
const newOption = createComponentOption();
setColumns([...option.columns]);
setColumns([...newOption.columns]);
}
}, [option, dataSet]);
useEffect(() => {
const observer = new ResizeObserver(entries => {
throttleResize(entries);
});
if (wrapperRef.current) {
observer.observe(wrapperRef.current);
}
return () => {
observer.disconnect();
};
}, []);
const throttleResize = _.throttle(entries => {
entries?.forEach(entry => {
setResizeObserver(entry.contentRect);
});
}, 300);
/**
*
* 위젯옵션과 데이터로
@@ -31,14 +50,9 @@ const TableBoard = props => {
};
return (
<Stack
sx={{
height: '100%',
minHeight: '100%',
overflow: 'hidden',
}}
>
<DataGridWrapper ref={wrapperRef}>
<DataGrid
resizeObserver={resizeObserver}
minBodyHeight={300}
bodyHeight={'fitToParent'}
data={dataSet}
@@ -47,7 +61,7 @@ const TableBoard = props => {
resizable: true,
}}
/>
</Stack>
</DataGridWrapper>
);
};

View File

@@ -75,7 +75,11 @@ const LineChart = props => {
const op = {
[axis + 'Axis']: {
type: 'category',
data: option[axis + 'Field'] ? aggrData.map(item => item[option[axis + 'Field']]) : '',
data: option[axis + 'Field']
? axis === 'x'
? aggrData.map(item => item[option[axis + 'Field']])
: aggrData.map(item => item[option[axis + 'Field']]).reverse()
: '',
},
series: newSeries,
grid: getGridSize(option.legendPosition),

View File

@@ -99,7 +99,11 @@ const MixedLinePieChart = props => {
const op = {
[axis + 'Axis']: {
type: 'category',
data: option[axis + 'Field'] ? aggrData.map(item => item[option[axis + 'Field']]) : '',
data: option[axis + 'Field']
? axis === 'x'
? aggrData.map(item => item[option[axis + 'Field']])
: aggrData.map(item => item[option[axis + 'Field']]).reverse()
: '',
},
series: newSeries,
grid: getGridSize(option.legendPosition),

View File

@@ -113,7 +113,7 @@ export const getAggregationData = (type, data, field) => {
if (data.length > 0 && (type === WIDGET_AGGREGATION.MIN || type === WIDGET_AGGREGATION.MAX)) {
result = Number(data[0][field]);
} else if (data.length > 0 && type === WIDGET_AGGREGATION.SUM && field) {
dataList = data.map(row => row[field]);
dataList = data.map(row => Number(row[field]));
fits = decimalFits(dataList);
}
switch (type) {

View File

@@ -55,7 +55,7 @@ export const WidgetEmpty = () => {
};
const WidgetViewer = props => {
const { title, widgetType, widgetOption, dataSet, isInvalidData } = props;
const { title, widgetType, widgetOption, dataSet, isInvalidData, size } = props;
const { showLoading, hideLoading } = useContext(LoadingContext);
const [module, setModule] = useState(null);
@@ -398,9 +398,10 @@ const WidgetViewer = props => {
left: 0,
right: 0,
maxWidth: 'fit-content',
height: 0,
height: '100%',
margin: 'auto',
transform: 'translateY(-50%)',
pointerEvents: 'none',
}}
/>
<DonutChart {...chartProps} />
@@ -419,9 +420,10 @@ const WidgetViewer = props => {
left: 0,
right: 0,
maxWidth: 'fit-content',
height: 0,
height: '100%',
margin: 'auto',
transform: 'translateY(-50%)',
pointerEvents: 'none',
}}
/>
<DonutChart
@@ -522,17 +524,28 @@ const WidgetViewer = props => {
sx={{
width: '100%',
height: 'calc(100% - 48px)',
// position: 'relative',
padding: '10px 40px 48px 40px',
pt: '10px',
pb: { xs: '25px', sm: '48px' },
px: { xs: '10px', sm: '40px' },
overflowY: 'auto',
}}
>
{isInvalidData ? <WidgetEmpty /> : module}
{/*<ReactECharts*/}
{/* option={componentOption}*/}
{/* style={{ height: '100%', maxHeight: '600px', width: '100%' }}*/}
{/* lazyUpdate={true}*/}
{/* notMerge={true}*/}
{/*/>*/}
<Stack
sx={{
position: 'relative',
width: '100%',
minWidth: { xs: `calc(${size} * 80px)`, sm: 0 },
height: '100%',
}}
>
{isInvalidData ? <WidgetEmpty /> : module}
{/*<ReactECharts*/}
{/* option={componentOption}*/}
{/* style={{ height: '100%', maxHeight: '600px', width: '100%' }}*/}
{/* lazyUpdate={true}*/}
{/* notMerge={true}*/}
{/*/>*/}
</Stack>
</Stack>
</Stack>
);

View File

@@ -7,7 +7,7 @@ import { useAlert } from 'react-alert';
import { SnackbarContext } from '@/contexts/AlertContext';
const WidgetWrapper = props => {
const { widgetOption, dataSetId } = props;
const { widgetOption, dataSetId, size } = props;
const { showLoading, hideLoading } = useContext(LoadingContext);
const [dataset, setDataset] = useState(null);
const snackbar = useAlert(SnackbarContext);
@@ -49,6 +49,7 @@ const WidgetWrapper = props => {
widgetOption={widgetOption.option}
dataSet={dataset}
isInvalidData={isInvalidData}
size={size}
/>
);
};