diff --git a/public/static/css/white.css b/public/static/css/white.css index 9fd933a..2d25310 100644 --- a/public/static/css/white.css +++ b/public/static/css/white.css @@ -135,7 +135,7 @@ margin-bottom: 1rem; } -.theme-default #browser-op button { +.theme-default #browser-op .left { margin: 0 1rem 0 0; } @@ -163,6 +163,36 @@ padding: 0; } +.theme-default #item-rows { + margin-top: 2rem; +} + +.theme-default #item-rows .name, .theme-default #item-rows .name a { + color: #34495e; + font-size: 1.8rem; + line-height: 2rem; + display: block; + margin: 2rem 0; +} + +.theme-default #item-rows .desc { + color: #95a5a6; + font-size: 1.4rem; + line-height: 2rem; + display: block; +} + +.theme-default #item-rows .hr { + height: 1px; + margin: 2rem 0; + background-color: #ecf0f1; +} + +.theme-default #item-rows .card { + padding: 0.5rem 0; + text-align: left; +} + .theme-default .item-cell { height: 5rem; } @@ -349,7 +379,7 @@ color: #34495e; background-color: #ecf0f6; border: solid 1px #95a5a6; - margin: 0.5rem 0 1rem 0; + /* margin: 0.5rem 0 1rem 0; */ } .captcha { diff --git a/src/client/web/package.json b/src/client/web/package.json index 1528010..2a339f0 100644 --- a/src/client/web/package.json +++ b/src/client/web/package.json @@ -55,6 +55,7 @@ "react": "^16.8.6", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.8.6", + "react-icons": "4.3.1", "react-svg": "^8.0.6", "throttle-debounce": "^2.1.0", "webpack-bundle-analyzer": "^4.4.2", diff --git a/src/client/web/src/components/__test__/pane_login.test.tsx b/src/client/web/src/components/__test__/pane_login.test.tsx index f0d8abc..af6347a 100644 --- a/src/client/web/src/components/__test__/pane_login.test.tsx +++ b/src/client/web/src/components/__test__/pane_login.test.tsx @@ -18,6 +18,7 @@ import { MockSettingsClient } from "../../client/settings_mock"; import { controlName as panelTabs } from "../root_frame"; import { settingsDialogCtrl } from "../layers"; import { settingsTabsCtrl } from "../dialog_settings"; +import { filesViewCtrl } from "../panel_files"; describe("Login", () => { initMockWorker(); @@ -122,6 +123,7 @@ describe("Login", () => { [settingsDialogCtrl]: ctrlOff, [settingsTabsCtrl]: "preferencePane", [sharingCtrl]: ctrlOff, + [filesViewCtrl]: "rows", }), options: Map>({ [panelTabs]: Set([ @@ -132,6 +134,7 @@ describe("Login", () => { [settingsDialogCtrl]: Set([ctrlOn, ctrlOff]), [settingsTabsCtrl]: Set(["preferencePane", "managementPane"]), [sharingCtrl]: Set([ctrlOn, ctrlOff]), + [filesViewCtrl]: Set(["rows", "table"]), }), }, }); diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index f3dcafd..babb3ca 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -4,6 +4,7 @@ import { UploadEntry } from "../worker/interface"; import { MsgPackage } from "../i18n/msger"; import { User, MetadataResp } from "../client"; import { settingsDialogCtrl } from "./layers"; +import { filesViewCtrl } from "./panel_files"; import { FilesProps } from "./panel_files"; import { UploadingsProps } from "./panel_uploadings"; import { SharingsProps } from "./panel_sharings"; @@ -112,6 +113,7 @@ export function initState(): ICoreState { [settingsDialogCtrl]: "off", [settingsTabsCtrl]: "preferencePane", [sharingCtrl]: "off", + [filesViewCtrl]: "rows", }), options: Map>({ [panelTabs]: Set([ @@ -122,6 +124,7 @@ export function initState(): ICoreState { [settingsDialogCtrl]: Set(["on", "off"]), [settingsTabsCtrl]: Set(["preferencePane", "managementPane"]), [sharingCtrl]: Set(["on", "off"]), + [filesViewCtrl]: Set(["rows", "table"]), }), }, }, diff --git a/src/client/web/src/components/layout/rows.tsx b/src/client/web/src/components/layout/rows.tsx new file mode 100644 index 0000000..ed74754 --- /dev/null +++ b/src/client/web/src/components/layout/rows.tsx @@ -0,0 +1,123 @@ +import * as React from "react"; +import { List, Map, Set } from "immutable"; + +import { RiArrowUpDownFill } from "@react-icons/all-files/ri/RiArrowUpDownFill"; + +import { Flexbox } from "./flexbox"; + +export interface Row { + elem: React.ReactNode; // element to display + val: Object; // original object value + sortVals: List; // sortable values in order +} + +export interface Props { + sortKeys: List; // display names in order for sorting + rows: List; + id?: string; + style?: React.CSSProperties; + className?: string; + updateRows?: (rows: Object) => void; // this is a callback which update state with re-sorted rows +} + +export interface State { + orders: List; // asc = true, desc = false +} + +export class Rows extends React.Component { + constructor(p: Props) { + super(p); + this.state = { + orders: p.sortKeys.map((_: string, i: number) => { + return false; + }), + }; + } + + sortRows = (key: number) => { + if (this.props.updateRows == null) { + return; + } + const sortOption = this.props.sortKeys.get(key); + if (sortOption == null) { + return; + } + const currentOrder = this.state.orders.get(key); + if (currentOrder == null) { + return; + } + const expectedOrder = !currentOrder; + + const sortedRows = this.props.rows.sort((row1: Row, row2: Row) => { + const val1 = row1.sortVals.get(key); + const val2 = row2.sortVals.get(key); + if (val1 == null || val2 == null) { + // elements without the sort key will be moved to the last + if (val1 == null && val2 != null) { + return 1; + } else if (val1 != null && val2 == null) { + return -1; + } + return 0; + } else if (val1 < val2) { + return expectedOrder ? -1 : 1; + } else if (val1 === val2) { + return 0; + } + return expectedOrder ? 1 : -1; + }); + + const sortedItems = sortedRows.map((row: Row): Object => { + return row.val; + }); + const newOrders = this.state.orders.set(key, !currentOrder); + this.setState({ orders: newOrders }); + this.props.updateRows(sortedItems); + }; + + render() { + const sortBtns = this.props.sortKeys.map( + (displayName: string, i: number): React.ReactNode => { + return ( + + ); + } + ); + + const bodyRows = this.props.rows.map( + (row: Row, i: number): React.ReactNode => { + return
{row.elem}
; + } + ); + + return ( +
+
+ , + {sortBtns}, + ])} + childrenStyles={List([{ flex: "0 0 auto" }, { flex: "0 0 auto" }])} + /> +
+ {bodyRows} +
+ ); + } +} diff --git a/src/client/web/src/components/panel_files.tsx b/src/client/web/src/components/panel_files.tsx index 7589af6..3a0a371 100644 --- a/src/client/web/src/components/panel_files.tsx +++ b/src/client/web/src/components/panel_files.tsx @@ -7,6 +7,11 @@ import { RiFolder2Fill } from "@react-icons/all-files/ri/RiFolder2Fill"; import { RiArchiveDrawerFill } from "@react-icons/all-files/ri/RiArchiveDrawerFill"; import { RiFile2Fill } from "@react-icons/all-files/ri/RiFile2Fill"; import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill"; +import { RiCheckboxFill } from "@react-icons/all-files/ri/RiCheckboxFill"; +import { RiCheckboxBlankFill } from "@react-icons/all-files/ri/RiCheckboxBlankFill"; +import { RiInformationFill } from "@react-icons/all-files/ri/RiInformationFill"; +import { BiTable } from "@react-icons/all-files/bi/BiTable"; +import { BiListUl } from "@react-icons/all-files/bi/BiListUl"; import { alertMsg, confirmMsg } from "../common/env"; import { getErrMsg } from "../common/utils"; @@ -17,10 +22,13 @@ import { MetadataResp, roleVisitor, roleAdmin } from "../client"; import { Flexbox } from "./layout/flexbox"; import { Container } from "./layout/container"; import { Table, Cell, Head } from "./layout/table"; +import { Rows, Row } from "./layout/rows"; import { Up } from "../worker/upload_mgr"; import { UploadEntry, UploadState } from "../worker/interface"; import { getIcon } from "./visual/icons"; +export const filesViewCtrl = "filesView"; + export interface Item { name: string; size: number; @@ -396,74 +404,10 @@ export class FilesPanel extends React.Component { this.props.update(updater().updateFilesInfo); }; - render() { - const showOp = this.props.login.userRole === roleVisitor ? "hidden" : ""; - const breadcrumb = this.props.filesInfo.dirPath.map( - (pathPart: string, key: number) => { - return ( - - ); - } - ); - - const ops = ( -
- - - -
, - -
- - -
, - ])} - childrenStyles={List([ - { flex: "0 0 70%" }, - { flex: "0 0 30%", justifyContent: "flex-end" }, - ])} - /> - - ); - - const sortedItems = this.props.filesInfo.items.sort( - (item1: MetadataResp, item2: MetadataResp) => { - if (item1.isDir && !item2.isDir) { - return -1; - } else if (!item1.isDir && item2.isDir) { - return 1; - } - return 0; - } - ); - + prepareTable = ( + sortedItems: List, + showOp: string + ): React.ReactNode => { const items = sortedItems.map((item: MetadataResp) => { const isSelected = this.state.selectedItems.has(item.name); const dirPath = this.props.filesInfo.dirPath.join("/"); @@ -590,6 +534,243 @@ export class FilesPanel extends React.Component { }, ]); + return ( + + ); + }; + + prepareRows = ( + sortedItems: List, + showOp: string + ): React.ReactNode => { + const sortKeys = List([ + this.props.msg.pkg.get("item.type"), + this.props.msg.pkg.get("item.name"), + ]); + + const rows = sortedItems.map((item: MetadataResp): Row => { + const isSelected = this.state.selectedItems.has(item.name); + const dirPath = this.props.filesInfo.dirPath.join("/"); + const itemPath = dirPath.endsWith("/") + ? `${dirPath}${item.name}` + : `${dirPath}/${item.name}`; + + const selectedIconColor = isSelected ? "cyan0-font" : "grey0-font"; + const descIconColor = this.state.showDetail.has(item.name) + ? "cyan0-font" + : "grey0-font"; + const icon = item.isDir ? ( + + ) : ( + + ); + const fileType = item.isDir + ? this.props.msg.pkg.get("item.type.folder") + : this.props.msg.pkg.get("item.type.file"); + + const name = item.isDir ? ( + this.gotoChild(item.name)}> + {item.name} + + ) : ( + + {item.name} + + ); + + const op = item.isDir ? ( +
+ this.select(item.name)} + /> +
+ ) : ( +
+ this.toggleDetail(item.name)} + /> + + this.select(item.name)} + /> +
+ ); + + const pathTitle = this.props.msg.pkg.get("item.path"); + const modTimeTitle = this.props.msg.pkg.get("item.modTime"); + const sizeTitle = this.props.msg.pkg.get("item.size"); + const fileTypeTitle = this.props.msg.pkg.get("item.type"); + const itemSize = FileSize(item.size, { round: 0 }); + + const compact = item.isDir + ? `${pathTitle}: ${itemPath} | ${modTimeTitle}: ${item.modTime}` + : `${pathTitle}: ${itemPath} | ${modTimeTitle}: ${item.modTime} | ${sizeTitle}: ${itemSize} | sha1: ${item.sha1}`; + const details = ( +
+
+ {pathTitle} + {itemPath} +
+
+ {modTimeTitle} + {item.modTime} +
+
+ {sizeTitle} + {itemSize} +
+
+ SHA1 + , + , + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> +
+
+ ); + const desc = this.state.showDetail.has(item.name) ? details : compact; + + const elem = ( +
+ +
+ {icon} + {`${fileTypeTitle}`} +   + {`- ${fileType}`} +
+
, +
{op}
, + ])} + childrenStyles={List([{}, { justifyContent: "flex-end" }])} + /> +
{name}
+
{desc}
+
+ + ); + + const sortVals = List([item.isDir ? "d" : "f", itemPath]); + return { + elem, + sortVals, + val: item, + }; + }); + + return ( + + ); + }; + + setView = (opt: string) => { + if (opt === "rows" || opt === "table") { + updater().setControlOption(filesViewCtrl, opt); + this.props.update(updater().updateUI); + return; + } + // TODO: log error + }; + + render() { + const showOp = this.props.login.userRole === roleVisitor ? "hidden" : ""; + const breadcrumb = this.props.filesInfo.dirPath.map( + (pathPart: string, key: number) => { + return ( + + ); + } + ); + + const ops = ( +
+ + + +
, + +
+ + +
, + ])} + childrenStyles={List([ + { flex: "0 0 70%" }, + { flex: "0 0 30%", justifyContent: "flex-end" }, + ])} + /> + + ); + + const viewType = this.props.ui.control.controls.get(filesViewCtrl); + const view = + viewType === "rows" ? ( +
+ {this.prepareRows(this.props.filesInfo.items, showOp)} +
+ ) : ( + this.prepareTable(this.props.filesInfo.items, showOp) + ); + const usedSpace = FileSize(parseInt(this.props.login.usedSpace, 10), { round: 0, }); @@ -619,7 +800,7 @@ export class FilesPanel extends React.Component { this.props.filesInfo.dirPath.join("/") ); }} - className="red-btn" + className="red-btn left" > {this.props.msg.pkg.get("browser.share.del")} @@ -627,7 +808,7 @@ export class FilesPanel extends React.Component { @@ -640,7 +821,7 @@ export class FilesPanel extends React.Component { @@ -652,14 +833,33 @@ export class FilesPanel extends React.Component { ) : null} , - - {`${this.props.msg.pkg.get( - "browser.used" - )} ${usedSpace} / ${spaceLimit}`} - , + { + this.setView("rows"); + }} + />, + { + this.setView("table"); + }} + />, + + + + , + ])} + />, ])} childrenStyles={List([ { flex: "0 0 auto" }, @@ -689,27 +889,19 @@ export class FilesPanel extends React.Component { /> , - - + + {`${this.props.msg.pkg.get( + "browser.used" + )} ${usedSpace} / ${spaceLimit}`} , ])} childrenStyles={List([{}, { justifyContent: "flex-end" }])} /> -
+ {view} ); diff --git a/src/client/web/src/components/state_updater.ts b/src/client/web/src/components/state_updater.ts index 8bf959b..f43a8ee 100644 --- a/src/client/web/src/components/state_updater.ts +++ b/src/client/web/src/components/state_updater.ts @@ -313,15 +313,37 @@ export class Updater { const isAuthed = this.props.login.authed; const isSharing = this.props.ui.control.controls.get(sharingCtrl) === ctrlOn; + const leftControls = this.props.ui.control.controls.filter( + (_: string, key: string): boolean => { + return ( + key !== panelTabs && + key !== settingsDialogCtrl && + key !== settingsTabsCtrl && + key !== sharingCtrl + ); + } + ); + const leftOpts = this.props.ui.control.options.filter( + (_: Set, key: string): boolean => { + return ( + key !== panelTabs && + key !== settingsDialogCtrl && + key !== settingsTabsCtrl && + key !== sharingCtrl + ); + } + ); + let newControls: Map = undefined; + let newOptions: Map> = undefined; if (isAuthed) { - this.props.ui.control.controls = Map({ + newControls = Map({ [panelTabs]: "filesPanel", [settingsDialogCtrl]: ctrlOff, [settingsTabsCtrl]: "preferencePane", [sharingCtrl]: isSharing ? ctrlOn : ctrlOff, }); - this.props.ui.control.options = Map>({ + newOptions = Map>({ [panelTabs]: Set([ "filesPanel", "uploadingsPanel", @@ -333,33 +355,33 @@ export class Updater { }); if (this.props.login.userRole == roleAdmin) { - this.props.ui.control.options = this.props.ui.control.options.set( + newOptions = newOptions.set( settingsTabsCtrl, Set(["preferencePane", "managementPane"]) ); } } else { if (isSharing) { - this.props.ui.control.controls = Map({ + newControls = Map({ [panelTabs]: "filesPanel", [settingsDialogCtrl]: ctrlHidden, [settingsTabsCtrl]: ctrlHidden, [sharingCtrl]: ctrlOn, }); - this.props.ui.control.options = Map>({ + newOptions = Map>({ [panelTabs]: Set(["filesPanel"]), [settingsDialogCtrl]: Set([ctrlHidden]), [settingsTabsCtrl]: Set([ctrlHidden]), [sharingCtrl]: Set([ctrlOn]), }); } else { - this.props.ui.control.controls = Map({ + newControls = Map({ [panelTabs]: ctrlHidden, [settingsDialogCtrl]: ctrlHidden, [settingsTabsCtrl]: ctrlHidden, [sharingCtrl]: ctrlOff, }); - this.props.ui.control.options = Map>({ + newOptions = Map>({ [panelTabs]: Set([ctrlHidden]), [settingsDialogCtrl]: Set([ctrlHidden]), [settingsTabsCtrl]: Set([ctrlHidden]), @@ -367,6 +389,9 @@ export class Updater { }); } } + + this.props.ui.control.controls = newControls.merge(leftControls); + this.props.ui.control.options = newOptions.merge(leftOpts); }; initStateForVisitor = async (): Promise => { diff --git a/src/client/web/src/components/visual/icons.tsx b/src/client/web/src/components/visual/icons.tsx index 2d93384..797a825 100644 --- a/src/client/web/src/components/visual/icons.tsx +++ b/src/client/web/src/components/visual/icons.tsx @@ -14,8 +14,9 @@ import { RiInformationFill } from "@react-icons/all-files/ri/RiInformationFill"; import { RiDeleteBin2Fill } from "@react-icons/all-files/ri/RiDeleteBin2Fill"; import { RiArchiveDrawerFill } from "@react-icons/all-files/ri/RiArchiveDrawerFill"; import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill"; - - +import { RiArrowUpDownFill } from "@react-icons/all-files/ri/RiArrowUpDownFill"; +import { BiTable } from "@react-icons/all-files/bi/BiTable"; +import { BiListUl } from "@react-icons/all-files/bi/BiListUl"; import { colorClass } from "./colors"; @@ -38,6 +39,9 @@ const icons = Map({ RiInformationFill: RiInformationFill, RiDeleteBin2Fill: RiDeleteBin2Fill, RiArchiveDrawerFill: RiArchiveDrawerFill, + RiArrowUpDownFill: RiArrowUpDownFill, + BiTable: BiTable, + BiListUl: BiListUl, }); export function getIconWithProps( diff --git a/src/client/web/src/i18n/en_US.ts b/src/client/web/src/i18n/en_US.ts index 22e71a6..ecf9f7b 100644 --- a/src/client/web/src/i18n/en_US.ts +++ b/src/client/web/src/i18n/en_US.ts @@ -118,4 +118,11 @@ export const msgs: Map = Map({ "err.server": "The operation failed in the server", "err.script.cors": "script error with CORS", "err.unknown": "unknown error", + "item.type": "Item Type", + "item.type.folder": "Folder", + "item.type.file": "File", + "item.name": "Item Name", + "item.path": "Path", + "item.modTime": "Mod Time", + "item.size": "Size", }); diff --git a/src/client/web/src/i18n/zh_CN.ts b/src/client/web/src/i18n/zh_CN.ts index 0bd9713..35a2458 100644 --- a/src/client/web/src/i18n/zh_CN.ts +++ b/src/client/web/src/i18n/zh_CN.ts @@ -115,4 +115,11 @@ export const msgs: Map = Map({ "err.server": "服务器端操作失败", "err.script.cors": "跨域脚本错误", "err.unknown": "未知错误", + "item.type": "类型", + "item.type.folder": "文件夹", + "item.type.file": "文件", + "item.name": "名称", + "item.path": "路径", + "item.modTime": "修改时间", + "item.size": "大小", }); diff --git a/yarn.lock b/yarn.lock index 6f90815..a2a6a5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1149,9 +1149,9 @@ chalk "^4.0.0" "@polka/url@^1.0.0-next.20": - version "1.0.0-next.20" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.20.tgz#111b5db0f501aa89b05076fa31f0ea0e0c292cd3" - integrity sha512-88p7+M0QGxKpmnkfXjS4V26AnoC/eiqZutE8GLdaI5X12NY75bXSdTY9NkmYb2Xyk1O+MmkuO6Frmsj84V6I8Q== + version "1.0.0-next.21" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" + integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== "@react-icons/all-files@^4.1.0": version "4.1.0" @@ -1615,7 +1615,12 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1: +acorn@^8.0.4: + version "8.6.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" + integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== + +acorn@^8.2.4, acorn@^8.4.1: version "8.5.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== @@ -2063,12 +2068,7 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@^7.0.0: +commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -3667,11 +3667,6 @@ mime-types@^2.1.12, mime-types@^2.1.27: dependencies: mime-db "1.49.0" -mime@^2.3.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -3696,6 +3691,11 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +mrmime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.0.tgz#14d387f0585a5233d291baba339b063752a2398b" + integrity sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4062,6 +4062,11 @@ react-dom@^16.8.6: prop-types "^15.6.2" scheduler "^0.19.1" +react-icons@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" + integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== + react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -4336,12 +4341,12 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: integrity sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q== sirv@^1.0.7: - version "1.0.17" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.17.tgz#86e2c63c612da5a1dace1c16c46f524aaa26ac45" - integrity sha512-qx9go5yraB7ekT7bCMqUHJ5jEaOC/GXBxUWv+jeWnb7WzHUFdcQPGWk7YmAwFBaQBrogpuSqd/azbC2lZRqqmw== + version "1.0.19" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" + integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== dependencies: "@polka/url" "^1.0.0-next.20" - mime "^2.3.1" + mrmime "^1.0.0" totalist "^1.0.0" sisteransi@^1.0.5: @@ -4855,14 +4860,14 @@ webidl-conversions@^6.1.0: integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== webpack-bundle-analyzer@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz#39898cf6200178240910d629705f0f3493f7d666" - integrity sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ== + version "4.5.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz#1b0eea2947e73528754a6f9af3e91b2b6e0f79d5" + integrity sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ== dependencies: acorn "^8.0.4" acorn-walk "^8.0.0" chalk "^4.1.0" - commander "^6.2.0" + commander "^7.2.0" gzip-size "^6.0.0" lodash "^4.17.20" opener "^1.5.2" @@ -5031,7 +5036,12 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.3.1, ws@^7.4.6: +ws@^7.3.1: + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + +ws@^7.4.6: version "7.5.5" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==