mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02:00
Merge remote-tracking branch 'origin/main' into 1-1263-integrations-frontend-adjustments
This commit is contained in:
commit
7466fe7051
@ -1 +1 @@
|
|||||||
18.17.0
|
18.17.1
|
||||||
|
@ -100,11 +100,11 @@
|
|||||||
"react-joyride": "^2.5.3",
|
"react-joyride": "^2.5.3",
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
"react-markdown": "^8.0.4",
|
"react-markdown": "^8.0.4",
|
||||||
"react-router-dom": "6.14.2",
|
"react-router-dom": "6.15.0",
|
||||||
"react-table": "7.8.0",
|
"react-table": "7.8.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
"react-timeago": "7.1.0",
|
"react-timeago": "7.1.0",
|
||||||
"sass": "1.64.2",
|
"sass": "1.65.1",
|
||||||
"semver": "7.5.4",
|
"semver": "7.5.4",
|
||||||
"swr": "2.2.0",
|
"swr": "2.2.0",
|
||||||
"tss-react": "4.8.8",
|
"tss-react": "4.8.8",
|
||||||
|
@ -90,7 +90,7 @@ export const adminRoutes: INavigationMenuItem[] = [
|
|||||||
{
|
{
|
||||||
path: '/admin/admin-invoices',
|
path: '/admin/admin-invoices',
|
||||||
title: 'Billing & invoices',
|
title: 'Billing & invoices',
|
||||||
menu: { adminSettings: true, mode: ['pro'], billing: true },
|
menu: { adminSettings: true, billing: true },
|
||||||
group: 'instance',
|
group: 'instance',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -115,7 +115,9 @@ const InvoiceList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
}
|
}
|
||||||
elseShow={<div>{isLoaded && 'No invoices to show.'}</div>}
|
elseShow={
|
||||||
|
<PageContent>{isLoaded && 'No invoices to show.'}</PageContent>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -55,6 +55,7 @@ export const AdminTabsMenu: VFC = () => {
|
|||||||
>
|
>
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<Tab
|
<Tab
|
||||||
|
sx={{ padding: 0 }}
|
||||||
key={tab.route}
|
key={tab.route}
|
||||||
value={tab.route?.split('/')?.[2]}
|
value={tab.route?.split('/')?.[2]}
|
||||||
label={
|
label={
|
||||||
|
@ -1,41 +1,21 @@
|
|||||||
import { Theme } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import React, { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
|
|
||||||
const createNavLinkStyle = (props: {
|
const StyledNavLink = styled(NavLink)(({ theme }) => ({
|
||||||
isActive: boolean;
|
|
||||||
theme: Theme;
|
|
||||||
}): React.CSSProperties => {
|
|
||||||
const navLinkStyle = {
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
padding: props.theme.spacing(1.5, 3),
|
padding: theme.spacing(0, 5),
|
||||||
};
|
'&.active': {
|
||||||
|
|
||||||
const activeNavLinkStyle: React.CSSProperties = {
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderRadius: '3px',
|
},
|
||||||
padding: props.theme.spacing(1.5, 3),
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
return props.isActive
|
|
||||||
? { ...navLinkStyle, ...activeNavLinkStyle }
|
|
||||||
: navLinkStyle;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CenteredNavLink: FC<{ to: string }> = ({ to, children }) => {
|
export const CenteredNavLink: FC<{ to: string }> = ({ to, children }) => {
|
||||||
const theme = useTheme();
|
return <StyledNavLink to={to}>{children}</StyledNavLink>;
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
to={to}
|
|
||||||
style={({ isActive }) => createNavLinkStyle({ isActive, theme })}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -43,6 +43,7 @@ export const Network = () => {
|
|||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</CenteredNavLink>
|
</CenteredNavLink>
|
||||||
}
|
}
|
||||||
|
sx={{ padding: 0 }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -97,6 +97,7 @@ export const RolesPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</CenteredNavLink>
|
</CenteredNavLink>
|
||||||
}
|
}
|
||||||
|
sx={{ padding: 0 }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -10,8 +10,19 @@ export const useAdminRoutes = () => {
|
|||||||
const showEnterpriseOptionsInPro = Boolean(
|
const showEnterpriseOptionsInPro = Boolean(
|
||||||
uiConfig?.flags?.frontendNavigationUpdate
|
uiConfig?.flags?.frontendNavigationUpdate
|
||||||
);
|
);
|
||||||
|
const routes = [...adminRoutes];
|
||||||
|
|
||||||
return adminRoutes
|
if (uiConfig.flags.UNLEASH_CLOUD) {
|
||||||
|
const adminBillingMenuItem = adminRoutes.findIndex(
|
||||||
|
route => route.title === 'Billing & invoices'
|
||||||
|
);
|
||||||
|
routes[adminBillingMenuItem] = {
|
||||||
|
...routes[adminBillingMenuItem],
|
||||||
|
path: '/admin/billing',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes
|
||||||
.filter(filterByConfig(uiConfig))
|
.filter(filterByConfig(uiConfig))
|
||||||
.filter(route =>
|
.filter(route =>
|
||||||
filterAdminRoutes(
|
filterAdminRoutes(
|
||||||
|
@ -1,41 +1,37 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { CircularProgress, Link } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
CircularProgress,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
import { Warning } from '@mui/icons-material';
|
import { Warning } from '@mui/icons-material';
|
||||||
import { AppsLinkList, styles as themeStyles } from 'component/common';
|
import { styles as themeStyles } from 'component/common';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { safeRegExp } from '@server/util/escape-regex';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import {
|
||||||
type PageQueryType = Partial<Record<'search', string>>;
|
SortableTableHeader,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
} from 'component/common/Table';
|
||||||
|
import { useGlobalFilter, useSortBy, useTable } from 'react-table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
|
||||||
export const ApplicationList = () => {
|
export const ApplicationList = () => {
|
||||||
const { applications, loading } = useApplications();
|
const { applications: data, loading } = useApplications();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const theme = useTheme();
|
||||||
const [searchValue, setSearchValue] = useState(
|
|
||||||
searchParams.get('search') || ''
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const tableState: PageQueryType = {};
|
|
||||||
if (searchValue) {
|
|
||||||
tableState.search = searchValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParams(tableState, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}, [searchValue, setSearchParams]);
|
|
||||||
|
|
||||||
const filteredApplications = useMemo(() => {
|
|
||||||
const regExp = safeRegExp(searchValue, 'i');
|
|
||||||
return searchValue
|
|
||||||
? applications?.filter(a => regExp.test(a.appName))
|
|
||||||
: applications;
|
|
||||||
}, [applications, searchValue]);
|
|
||||||
|
|
||||||
const renderNoApplications = () => (
|
const renderNoApplications = () => (
|
||||||
<>
|
<>
|
||||||
@ -56,25 +52,115 @@ export const ApplicationList = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!filteredApplications) {
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [{ id: 'name', desc: false }],
|
||||||
|
hiddenColumns: ['description', 'sortOrder'],
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'Icon',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { icon },
|
||||||
|
},
|
||||||
|
}: any) => (
|
||||||
|
<IconCell
|
||||||
|
icon={
|
||||||
|
<Avatar>
|
||||||
|
<Icon>{icon}</Icon>
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'appName',
|
||||||
|
width: '50%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { appName, description },
|
||||||
|
},
|
||||||
|
}: any) => (
|
||||||
|
<LinkCell
|
||||||
|
title={appName}
|
||||||
|
to={`/applications/${appName}`}
|
||||||
|
subtitle={description}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Project(environment)',
|
||||||
|
accessor: 'usage',
|
||||||
|
width: '50%',
|
||||||
|
Cell: () => (
|
||||||
|
<TextCell>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
not connected
|
||||||
|
</Typography>
|
||||||
|
</TextCell>
|
||||||
|
),
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'sortOrder',
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
sortType: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
setGlobalFilter,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
return <CircularProgress variant="indeterminate" />;
|
return <CircularProgress variant="indeterminate" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let applicationCount =
|
|
||||||
filteredApplications.length < applications.length
|
|
||||||
? `${filteredApplications.length} of ${applications.length}`
|
|
||||||
: applications.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContent
|
<PageContent
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={`Applications (${applicationCount})`}
|
title={`Applications (${rows.length})`}
|
||||||
actions={
|
actions={
|
||||||
<Search
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={globalFilter}
|
||||||
onChange={setSearchValue}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -82,8 +168,37 @@ export const ApplicationList = () => {
|
|||||||
>
|
>
|
||||||
<div className={themeStyles.fullwidth}>
|
<div className={themeStyles.fullwidth}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={filteredApplications.length > 0}
|
condition={data.length > 0}
|
||||||
show={<AppsLinkList apps={filteredApplications} />}
|
show={
|
||||||
|
<SearchHighlightProvider value={globalFilter}>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
<SortableTableHeader
|
||||||
|
headerGroups={headerGroups}
|
||||||
|
/>
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
{...row.getRowProps()}
|
||||||
|
>
|
||||||
|
{row.cells.map(cell => (
|
||||||
|
<TableCell
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
>
|
||||||
|
{cell.render(
|
||||||
|
'Cell'
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={loading}
|
condition={loading}
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { CircularProgress, Link } from '@mui/material';
|
||||||
|
import { Warning } from '@mui/icons-material';
|
||||||
|
import { AppsLinkList, styles as themeStyles } from 'component/common';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { safeRegExp } from '@server/util/escape-regex';
|
||||||
|
|
||||||
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
|
export const OldApplicationList = () => {
|
||||||
|
const { applications, loading } = useApplications();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [searchValue, setSearchValue] = useState(
|
||||||
|
searchParams.get('search') || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tableState: PageQueryType = {};
|
||||||
|
if (searchValue) {
|
||||||
|
tableState.search = searchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(tableState, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}, [searchValue, setSearchParams]);
|
||||||
|
|
||||||
|
const filteredApplications = useMemo(() => {
|
||||||
|
const regExp = safeRegExp(searchValue, 'i');
|
||||||
|
return searchValue
|
||||||
|
? applications?.filter(a => regExp.test(a.appName))
|
||||||
|
: applications;
|
||||||
|
}, [applications, searchValue]);
|
||||||
|
|
||||||
|
const renderNoApplications = () => (
|
||||||
|
<>
|
||||||
|
<section style={{ textAlign: 'center' }}>
|
||||||
|
<Warning titleAccess="Warning" /> <br />
|
||||||
|
<br />
|
||||||
|
Oh snap, it does not seem like you have connected any
|
||||||
|
applications. To connect your application to Unleash you will
|
||||||
|
require a Client SDK.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
You can read more about how to use Unleash in your application
|
||||||
|
in the{' '}
|
||||||
|
<Link href="https://docs.getunleash.io/docs/sdks/">
|
||||||
|
documentation.
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!filteredApplications) {
|
||||||
|
return <CircularProgress variant="indeterminate" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let applicationCount =
|
||||||
|
filteredApplications.length < applications.length
|
||||||
|
? `${filteredApplications.length} of ${applications.length}`
|
||||||
|
: applications.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContent
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title={`Applications (${applicationCount})`}
|
||||||
|
actions={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={themeStyles.fullwidth}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={filteredApplications.length > 0}
|
||||||
|
show={<AppsLinkList apps={filteredApplications} />}
|
||||||
|
elseShow={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={loading}
|
||||||
|
show={<div>...loading</div>}
|
||||||
|
elseShow={renderNoApplications()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Avatar, CircularProgress, Icon, Link } from '@mui/material';
|
||||||
|
import { Warning } from '@mui/icons-material';
|
||||||
|
import { styles as themeStyles } from 'component/common';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import {
|
||||||
|
SortableTableHeader,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
} from '../../common/Table';
|
||||||
|
import { useGlobalFilter, useSortBy, useTable } from 'react-table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ApplicationList } from './ApplicationList';
|
||||||
|
import { OldApplicationList } from './OldApplicationList';
|
||||||
|
|
||||||
|
export const TemporaryApplicationListWrapper = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig.flags.newApplicationList)}
|
||||||
|
show={<ApplicationList />}
|
||||||
|
elseShow={<OldApplicationList />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -98,9 +98,9 @@ export const NavigationMenu = ({
|
|||||||
}
|
}
|
||||||
arrow
|
arrow
|
||||||
placement="left"
|
placement="left"
|
||||||
|
key={option.path}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={option.path}
|
|
||||||
component={StyledLink}
|
component={StyledLink}
|
||||||
to={option.path}
|
to={option.path}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
|
@ -19,7 +19,6 @@ import EditProject from 'component/project/Project/EditProject/EditProject';
|
|||||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
||||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||||
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
|
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
|
||||||
import { ApplicationList } from 'component/application/ApplicationList/ApplicationList';
|
|
||||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||||
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
||||||
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
||||||
@ -44,6 +43,7 @@ import { LazyAdmin } from 'component/admin/LazyAdmin';
|
|||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||||
|
import { TemporaryApplicationListWrapper } from 'component/application/ApplicationList/TemporaryApplicationListWrapper';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -179,7 +179,7 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/applications',
|
path: '/applications',
|
||||||
title: 'Applications',
|
title: 'Applications',
|
||||||
component: ApplicationList,
|
component: TemporaryApplicationListWrapper,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
},
|
},
|
||||||
|
@ -58,6 +58,7 @@ export interface IFlags {
|
|||||||
segmentChangeRequests?: boolean;
|
segmentChangeRequests?: boolean;
|
||||||
changeRequestReject?: boolean;
|
changeRequestReject?: boolean;
|
||||||
lastSeenByEnvironment?: boolean;
|
lastSeenByEnvironment?: boolean;
|
||||||
|
newApplicationList?: boolean;
|
||||||
integrationsRework?: boolean;
|
integrationsRework?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2275,10 +2275,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||||
|
|
||||||
"@remix-run/router@1.7.2":
|
"@remix-run/router@1.8.0":
|
||||||
version "1.7.2"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||||
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
|
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||||
|
|
||||||
"@rollup/plugin-commonjs@~22.0.2":
|
"@rollup/plugin-commonjs@~22.0.2":
|
||||||
version "22.0.2"
|
version "22.0.2"
|
||||||
@ -8261,20 +8261,20 @@ react-refresh@^0.14.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
||||||
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
|
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
|
||||||
|
|
||||||
react-router-dom@6.14.2:
|
react-router-dom@6.15.0:
|
||||||
version "6.14.2"
|
version "6.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.2.tgz#88f520118b91aa60233bd08dbd3fdcaea3a68488"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
|
||||||
integrity sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==
|
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.2"
|
"@remix-run/router" "1.8.0"
|
||||||
react-router "6.14.2"
|
react-router "6.15.0"
|
||||||
|
|
||||||
react-router@6.14.2:
|
react-router@6.15.0:
|
||||||
version "6.14.2"
|
version "6.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||||
integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==
|
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.2"
|
"@remix-run/router" "1.8.0"
|
||||||
|
|
||||||
react-shallow-renderer@^16.13.1:
|
react-shallow-renderer@^16.13.1:
|
||||||
version "16.15.0"
|
version "16.15.0"
|
||||||
@ -8617,10 +8617,10 @@ safe-stable-stringify@^1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
sass@1.64.2:
|
sass@1.65.1:
|
||||||
version "1.64.2"
|
version "1.65.1"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.64.2.tgz#0d9805ad6acf31c59c3acc725fcfb91b7fcc6909"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.65.1.tgz#8f283b0c26335a88246a448d22e1342ba2ea1432"
|
||||||
integrity sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==
|
integrity sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
|
@ -57,6 +57,14 @@ const slackAppDefinition: IAddonDefinition = {
|
|||||||
required: false,
|
required: false,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'alwaysPostToDefault',
|
||||||
|
displayName: 'Always post to default channels',
|
||||||
|
description: `If set to 'true' or 'yes', the app will always post events to the default channels, even if the feature toggle has slack tags`,
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
sensitive: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
events: [
|
events: [
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
|
@ -23,6 +23,7 @@ import { IEvent } from '../types/events';
|
|||||||
interface ISlackAppAddonParameters {
|
interface ISlackAppAddonParameters {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
defaultChannels: string;
|
defaultChannels: string;
|
||||||
|
alwaysPostToDefault: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SlackAppAddon extends Addon {
|
export default class SlackAppAddon extends Addon {
|
||||||
@ -45,16 +46,26 @@ export default class SlackAppAddon extends Addon {
|
|||||||
parameters: ISlackAppAddonParameters,
|
parameters: ISlackAppAddonParameters,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { accessToken, defaultChannels } = parameters;
|
const { accessToken, defaultChannels, alwaysPostToDefault } =
|
||||||
|
parameters;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
this.logger.warn('No access token provided.');
|
this.logger.warn('No access token provided.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let postToDefault =
|
||||||
|
alwaysPostToDefault === 'true' || alwaysPostToDefault === 'yes';
|
||||||
|
this.logger.debug(`Post to default was set to ${postToDefault}`);
|
||||||
const taggedChannels = this.findTaggedChannels(event);
|
const taggedChannels = this.findTaggedChannels(event);
|
||||||
const eventChannels = taggedChannels.length
|
let eventChannels: string[];
|
||||||
|
if (postToDefault) {
|
||||||
|
eventChannels = taggedChannels.concat(
|
||||||
|
this.getDefaultChannels(defaultChannels),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eventChannels = taggedChannels.length
|
||||||
? taggedChannels
|
? taggedChannels
|
||||||
: this.getDefaultChannels(defaultChannels);
|
: this.getDefaultChannels(defaultChannels);
|
||||||
|
}
|
||||||
|
|
||||||
if (!eventChannels.length) {
|
if (!eventChannels.length) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
|
@ -335,6 +335,11 @@ test('validate import data', async () => {
|
|||||||
'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
|
'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
|
||||||
affectedItems: [archivedFeature],
|
affectedItems: [archivedFeature],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'The following features already exist in this project and will be overwritten:',
|
||||||
|
affectedItems: ['existing_feature'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
permissions: [
|
permissions: [
|
||||||
{
|
{
|
||||||
|
@ -151,6 +151,7 @@ export default class ExportImportService {
|
|||||||
unsupportedContextFields,
|
unsupportedContextFields,
|
||||||
archivedFeatures,
|
archivedFeatures,
|
||||||
otherProjectFeatures,
|
otherProjectFeatures,
|
||||||
|
existingProjectFeatures,
|
||||||
missingPermissions,
|
missingPermissions,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getUnsupportedStrategies(dto),
|
this.getUnsupportedStrategies(dto),
|
||||||
@ -158,6 +159,7 @@ export default class ExportImportService {
|
|||||||
this.getUnsupportedContextFields(dto),
|
this.getUnsupportedContextFields(dto),
|
||||||
this.getArchivedFeatures(dto),
|
this.getArchivedFeatures(dto),
|
||||||
this.getOtherProjectFeatures(dto),
|
this.getOtherProjectFeatures(dto),
|
||||||
|
this.getExistingProjectFeatures(dto),
|
||||||
this.importPermissionsService.getMissingPermissions(
|
this.importPermissionsService.getMissingPermissions(
|
||||||
dto,
|
dto,
|
||||||
user,
|
user,
|
||||||
@ -176,6 +178,7 @@ export default class ExportImportService {
|
|||||||
const warnings = ImportValidationMessages.compileWarnings(
|
const warnings = ImportValidationMessages.compileWarnings(
|
||||||
usedCustomStrategies,
|
usedCustomStrategies,
|
||||||
archivedFeatures,
|
archivedFeatures,
|
||||||
|
existingProjectFeatures,
|
||||||
);
|
);
|
||||||
const permissions =
|
const permissions =
|
||||||
ImportValidationMessages.compilePermissionErrors(
|
ImportValidationMessages.compilePermissionErrors(
|
||||||
@ -299,7 +302,7 @@ export default class ExportImportService {
|
|||||||
this.contextService.createContextField(
|
this.contextService.createContextField(
|
||||||
{
|
{
|
||||||
name: contextField.name,
|
name: contextField.name,
|
||||||
description: contextField.description,
|
description: contextField.description || '',
|
||||||
legalValues: contextField.legalValues,
|
legalValues: contextField.legalValues,
|
||||||
stickiness: contextField.stickiness,
|
stickiness: contextField.stickiness,
|
||||||
},
|
},
|
||||||
@ -529,6 +532,15 @@ export default class ExportImportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getExistingProjectFeatures(dto: ImportTogglesSchema) {
|
||||||
|
const existingProjectsFeatures =
|
||||||
|
await this.importTogglesStore.getFeaturesInProject(
|
||||||
|
dto.data.features.map((feature) => feature.name),
|
||||||
|
dto.project,
|
||||||
|
);
|
||||||
|
return existingProjectsFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
private async getNewTagTypes(dto: ImportTogglesSchema) {
|
private async getNewTagTypes(dto: ImportTogglesSchema) {
|
||||||
const existingTagTypes = (await this.tagTypeService.getAll()).map(
|
const existingTagTypes = (await this.tagTypeService.getAll()).map(
|
||||||
(tagType) => tagType.name,
|
(tagType) => tagType.name,
|
||||||
|
@ -11,6 +11,11 @@ export interface IImportTogglesStore {
|
|||||||
project: string,
|
project: string,
|
||||||
): Promise<{ name: string; project: string }[]>;
|
): Promise<{ name: string; project: string }[]>;
|
||||||
|
|
||||||
|
getFeaturesInProject(
|
||||||
|
featureNames: string[],
|
||||||
|
project: string,
|
||||||
|
): Promise<string[]>;
|
||||||
|
|
||||||
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
||||||
|
|
||||||
strategiesExistForFeatures(
|
strategiesExistForFeatures(
|
||||||
|
@ -74,6 +74,18 @@ export class ImportTogglesStore implements IImportTogglesStore {
|
|||||||
return rows.map((row) => ({ name: row.name, project: row.project }));
|
return rows.map((row) => ({ name: row.name, project: row.project }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFeaturesInProject(
|
||||||
|
featureNames: string[],
|
||||||
|
project: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const rows = await this.db(T.features)
|
||||||
|
.select(['name', 'project'])
|
||||||
|
.where('project', project)
|
||||||
|
.where('archived_at', null)
|
||||||
|
.whereIn('name', featureNames);
|
||||||
|
return rows.map((row) => row.name);
|
||||||
|
}
|
||||||
|
|
||||||
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
||||||
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ export class ImportValidationMessages {
|
|||||||
static compileWarnings(
|
static compileWarnings(
|
||||||
usedCustomStrategies: string[],
|
usedCustomStrategies: string[],
|
||||||
archivedFeatures: string[],
|
archivedFeatures: string[],
|
||||||
|
existingFeatures: string[],
|
||||||
): ImportTogglesValidateItemSchema[] {
|
): ImportTogglesValidateItemSchema[] {
|
||||||
const warnings: ImportTogglesValidateItemSchema[] = [];
|
const warnings: ImportTogglesValidateItemSchema[] = [];
|
||||||
if (usedCustomStrategies.length > 0) {
|
if (usedCustomStrategies.length > 0) {
|
||||||
@ -89,6 +90,13 @@ export class ImportValidationMessages {
|
|||||||
affectedItems: archivedFeatures,
|
affectedItems: archivedFeatures,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (existingFeatures.length > 0) {
|
||||||
|
warnings.push({
|
||||||
|
message:
|
||||||
|
'The following features already exist in this project and will be overwritten:',
|
||||||
|
affectedItems: existingFeatures,
|
||||||
|
});
|
||||||
|
}
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export type IFlagKey =
|
|||||||
| 'segmentChangeRequests'
|
| 'segmentChangeRequests'
|
||||||
| 'changeRequestReject'
|
| 'changeRequestReject'
|
||||||
| 'customRootRolesKillSwitch'
|
| 'customRootRolesKillSwitch'
|
||||||
|
| 'newApplicationList'
|
||||||
| 'integrationsRework';
|
| 'integrationsRework';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS client_applications_usage (
|
||||||
|
app_name VARCHAR(255) REFERENCES client_applications(app_name) ON DELETE CASCADE,
|
||||||
|
project VARCHAR(255) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
environment VARCHAR(100) REFERENCES environments(name) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY(app_name, project, environment)
|
||||||
|
) ;
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DROP TABLE IF EXISTS client_applications_usage;
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
@ -45,6 +45,7 @@ process.nextTick(async () => {
|
|||||||
frontendNavigationUpdate: true,
|
frontendNavigationUpdate: true,
|
||||||
lastSeenByEnvironment: true,
|
lastSeenByEnvironment: true,
|
||||||
segmentChangeRequests: true,
|
segmentChangeRequests: true,
|
||||||
|
newApplicationList: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -193,7 +193,7 @@ test('all tags are listed in the root "tags" list', async () => {
|
|||||||
expect(invalidTags).toStrictEqual({});
|
expect(invalidTags).toStrictEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('all API operations have summaries and descriptions', async () => {
|
test('all API operations have non-empty summaries and descriptions', async () => {
|
||||||
const { body: spec } = await app.request
|
const { body: spec } = await app.request
|
||||||
.get('/docs/openapi.json')
|
.get('/docs/openapi.json')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
@ -203,8 +203,8 @@ test('all API operations have summaries and descriptions', async () => {
|
|||||||
return Object.entries(data)
|
return Object.entries(data)
|
||||||
.map(([verb, operationDescription]) => {
|
.map(([verb, operationDescription]) => {
|
||||||
if (
|
if (
|
||||||
'summary' in operationDescription &&
|
operationDescription.summary &&
|
||||||
'description' in operationDescription
|
operationDescription.description
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,13 +8,21 @@ This website is built using [Docusaurus 2](https://docusaurus.io/), a modern sta
|
|||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Generate Open API docs
|
||||||
|
|
||||||
|
```console
|
||||||
|
yarn generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate the Open API docs that live at Reference documentation > APIs > OpenAPI
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
```console
|
```console
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
Start a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
"@tsconfig/docusaurus": "2.0.0",
|
"@tsconfig/docusaurus": "2.0.0",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"enhanced-resolve": "5.15.0",
|
"enhanced-resolve": "5.15.0",
|
||||||
"react-router": "6.14.2",
|
"react-router": "6.15.0",
|
||||||
"replace-in-file": "7.0.1",
|
"replace-in-file": "7.0.1",
|
||||||
"storybook-addon-root-attribute": "1.0.2",
|
"storybook-addon-root-attribute": "1.0.2",
|
||||||
"typescript": "4.8.4"
|
"typescript": "4.8.4"
|
||||||
|
@ -3291,10 +3291,10 @@
|
|||||||
redux-thunk "^2.4.2"
|
redux-thunk "^2.4.2"
|
||||||
reselect "^4.1.7"
|
reselect "^4.1.7"
|
||||||
|
|
||||||
"@remix-run/router@1.7.2":
|
"@remix-run/router@1.8.0":
|
||||||
version "1.7.2"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||||
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
|
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||||
|
|
||||||
"@sideway/address@^4.1.3":
|
"@sideway/address@^4.1.3":
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
@ -13714,12 +13714,12 @@ react-router@5.3.4, react-router@^5.3.3:
|
|||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
react-router@6.14.2:
|
react-router@6.15.0:
|
||||||
version "6.14.2"
|
version "6.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||||
integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==
|
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.2"
|
"@remix-run/router" "1.8.0"
|
||||||
|
|
||||||
react-textarea-autosize@^8.3.2:
|
react-textarea-autosize@^8.3.2:
|
||||||
version "8.4.0"
|
version "8.4.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user