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-linkify": "^1.0.0-alpha",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-router-dom": "6.14.2",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-table": "7.8.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"react-timeago": "7.1.0",
|
||||
"sass": "1.64.2",
|
||||
"sass": "1.65.1",
|
||||
"semver": "7.5.4",
|
||||
"swr": "2.2.0",
|
||||
"tss-react": "4.8.8",
|
||||
|
@ -90,7 +90,7 @@ export const adminRoutes: INavigationMenuItem[] = [
|
||||
{
|
||||
path: '/admin/admin-invoices',
|
||||
title: 'Billing & invoices',
|
||||
menu: { adminSettings: true, mode: ['pro'], billing: true },
|
||||
menu: { adminSettings: true, billing: true },
|
||||
group: 'instance',
|
||||
},
|
||||
{
|
||||
|
@ -115,7 +115,9 @@ const InvoiceList = () => {
|
||||
</div>
|
||||
</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 => (
|
||||
<Tab
|
||||
sx={{ padding: 0 }}
|
||||
key={tab.route}
|
||||
value={tab.route?.split('/')?.[2]}
|
||||
label={
|
||||
|
@ -1,41 +1,21 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { FC } from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
const createNavLinkStyle = (props: {
|
||||
isActive: boolean;
|
||||
theme: Theme;
|
||||
}): React.CSSProperties => {
|
||||
const navLinkStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
padding: props.theme.spacing(1.5, 3),
|
||||
};
|
||||
|
||||
const activeNavLinkStyle: React.CSSProperties = {
|
||||
const StyledNavLink = styled(NavLink)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
padding: theme.spacing(0, 5),
|
||||
'&.active': {
|
||||
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 }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
style={({ isActive }) => createNavLinkStyle({ isActive, theme })}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
return <StyledNavLink to={to}>{children}</StyledNavLink>;
|
||||
};
|
||||
|
@ -43,6 +43,7 @@ export const Network = () => {
|
||||
<span>{label}</span>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
sx={{ padding: 0 }}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
@ -97,6 +97,7 @@ export const RolesPage = () => {
|
||||
</span>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
sx={{ padding: 0 }}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
@ -10,8 +10,19 @@ export const useAdminRoutes = () => {
|
||||
const showEnterpriseOptionsInPro = Boolean(
|
||||
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(route =>
|
||||
filterAdminRoutes(
|
||||
|
@ -1,41 +1,37 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CircularProgress, Link } from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
CircularProgress,
|
||||
Icon,
|
||||
Link,
|
||||
styled,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/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 { 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>>;
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import {
|
||||
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 = () => {
|
||||
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 { applications: data, loading } = useApplications();
|
||||
const theme = useTheme();
|
||||
|
||||
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" />;
|
||||
}
|
||||
|
||||
let applicationCount =
|
||||
filteredApplications.length < applications.length
|
||||
? `${filteredApplications.length} of ${applications.length}`
|
||||
: applications.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContent
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Applications (${applicationCount})`}
|
||||
title={`Applications (${rows.length})`}
|
||||
actions={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
initialValue={globalFilter}
|
||||
onChange={setGlobalFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -82,8 +168,37 @@ export const ApplicationList = () => {
|
||||
>
|
||||
<div className={themeStyles.fullwidth}>
|
||||
<ConditionallyRender
|
||||
condition={filteredApplications.length > 0}
|
||||
show={<AppsLinkList apps={filteredApplications} />}
|
||||
condition={data.length > 0}
|
||||
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={
|
||||
<ConditionallyRender
|
||||
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
|
||||
placement="left"
|
||||
key={option.path}
|
||||
>
|
||||
<MenuItem
|
||||
key={option.path}
|
||||
component={StyledLink}
|
||||
to={option.path}
|
||||
onClick={handleClose}
|
||||
|
@ -19,7 +19,6 @@ import EditProject from 'component/project/Project/EditProject/EditProject';
|
||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
|
||||
import { ApplicationList } from 'component/application/ApplicationList/ApplicationList';
|
||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
||||
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 { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||
import { TemporaryApplicationListWrapper } from 'component/application/ApplicationList/TemporaryApplicationListWrapper';
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
// Splash
|
||||
@ -179,7 +179,7 @@ export const routes: IRoute[] = [
|
||||
{
|
||||
path: '/applications',
|
||||
title: 'Applications',
|
||||
component: ApplicationList,
|
||||
component: TemporaryApplicationListWrapper,
|
||||
type: 'protected',
|
||||
menu: { mobile: true, advanced: true },
|
||||
},
|
||||
|
@ -58,6 +58,7 @@ export interface IFlags {
|
||||
segmentChangeRequests?: boolean;
|
||||
changeRequestReject?: boolean;
|
||||
lastSeenByEnvironment?: boolean;
|
||||
newApplicationList?: boolean;
|
||||
integrationsRework?: boolean;
|
||||
}
|
||||
|
||||
|
@ -2275,10 +2275,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@remix-run/router@1.7.2":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
|
||||
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
|
||||
"@remix-run/router@1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||
|
||||
"@rollup/plugin-commonjs@~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"
|
||||
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
|
||||
|
||||
react-router-dom@6.14.2:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.2.tgz#88f520118b91aa60233bd08dbd3fdcaea3a68488"
|
||||
integrity sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==
|
||||
react-router-dom@6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
|
||||
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.7.2"
|
||||
react-router "6.14.2"
|
||||
"@remix-run/router" "1.8.0"
|
||||
react-router "6.15.0"
|
||||
|
||||
react-router@6.14.2:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300"
|
||||
integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==
|
||||
react-router@6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.7.2"
|
||||
"@remix-run/router" "1.8.0"
|
||||
|
||||
react-shallow-renderer@^16.13.1:
|
||||
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"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@1.64.2:
|
||||
version "1.64.2"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.64.2.tgz#0d9805ad6acf31c59c3acc725fcfb91b7fcc6909"
|
||||
integrity sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==
|
||||
sass@1.65.1:
|
||||
version "1.65.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.65.1.tgz#8f283b0c26335a88246a448d22e1342ba2ea1432"
|
||||
integrity sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
|
@ -57,6 +57,14 @@ const slackAppDefinition: IAddonDefinition = {
|
||||
required: 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: [
|
||||
FEATURE_CREATED,
|
||||
|
@ -23,6 +23,7 @@ import { IEvent } from '../types/events';
|
||||
interface ISlackAppAddonParameters {
|
||||
accessToken: string;
|
||||
defaultChannels: string;
|
||||
alwaysPostToDefault: string;
|
||||
}
|
||||
|
||||
export default class SlackAppAddon extends Addon {
|
||||
@ -45,16 +46,26 @@ export default class SlackAppAddon extends Addon {
|
||||
parameters: ISlackAppAddonParameters,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { accessToken, defaultChannels } = parameters;
|
||||
const { accessToken, defaultChannels, alwaysPostToDefault } =
|
||||
parameters;
|
||||
if (!accessToken) {
|
||||
this.logger.warn('No access token provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
let postToDefault =
|
||||
alwaysPostToDefault === 'true' || alwaysPostToDefault === 'yes';
|
||||
this.logger.debug(`Post to default was set to ${postToDefault}`);
|
||||
const taggedChannels = this.findTaggedChannels(event);
|
||||
const eventChannels = taggedChannels.length
|
||||
? taggedChannels
|
||||
: this.getDefaultChannels(defaultChannels);
|
||||
let eventChannels: string[];
|
||||
if (postToDefault) {
|
||||
eventChannels = taggedChannels.concat(
|
||||
this.getDefaultChannels(defaultChannels),
|
||||
);
|
||||
} else {
|
||||
eventChannels = taggedChannels.length
|
||||
? taggedChannels
|
||||
: this.getDefaultChannels(defaultChannels);
|
||||
}
|
||||
|
||||
if (!eventChannels.length) {
|
||||
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:',
|
||||
affectedItems: [archivedFeature],
|
||||
},
|
||||
{
|
||||
message:
|
||||
'The following features already exist in this project and will be overwritten:',
|
||||
affectedItems: ['existing_feature'],
|
||||
},
|
||||
],
|
||||
permissions: [
|
||||
{
|
||||
|
@ -151,6 +151,7 @@ export default class ExportImportService {
|
||||
unsupportedContextFields,
|
||||
archivedFeatures,
|
||||
otherProjectFeatures,
|
||||
existingProjectFeatures,
|
||||
missingPermissions,
|
||||
] = await Promise.all([
|
||||
this.getUnsupportedStrategies(dto),
|
||||
@ -158,6 +159,7 @@ export default class ExportImportService {
|
||||
this.getUnsupportedContextFields(dto),
|
||||
this.getArchivedFeatures(dto),
|
||||
this.getOtherProjectFeatures(dto),
|
||||
this.getExistingProjectFeatures(dto),
|
||||
this.importPermissionsService.getMissingPermissions(
|
||||
dto,
|
||||
user,
|
||||
@ -176,6 +178,7 @@ export default class ExportImportService {
|
||||
const warnings = ImportValidationMessages.compileWarnings(
|
||||
usedCustomStrategies,
|
||||
archivedFeatures,
|
||||
existingProjectFeatures,
|
||||
);
|
||||
const permissions =
|
||||
ImportValidationMessages.compilePermissionErrors(
|
||||
@ -299,7 +302,7 @@ export default class ExportImportService {
|
||||
this.contextService.createContextField(
|
||||
{
|
||||
name: contextField.name,
|
||||
description: contextField.description,
|
||||
description: contextField.description || '',
|
||||
legalValues: contextField.legalValues,
|
||||
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) {
|
||||
const existingTagTypes = (await this.tagTypeService.getAll()).map(
|
||||
(tagType) => tagType.name,
|
||||
|
@ -11,6 +11,11 @@ export interface IImportTogglesStore {
|
||||
project: string,
|
||||
): Promise<{ name: string; project: string }[]>;
|
||||
|
||||
getFeaturesInProject(
|
||||
featureNames: string[],
|
||||
project: string,
|
||||
): Promise<string[]>;
|
||||
|
||||
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
||||
|
||||
strategiesExistForFeatures(
|
||||
|
@ -74,6 +74,18 @@ export class ImportTogglesStore implements IImportTogglesStore {
|
||||
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> {
|
||||
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ export class ImportValidationMessages {
|
||||
static compileWarnings(
|
||||
usedCustomStrategies: string[],
|
||||
archivedFeatures: string[],
|
||||
existingFeatures: string[],
|
||||
): ImportTogglesValidateItemSchema[] {
|
||||
const warnings: ImportTogglesValidateItemSchema[] = [];
|
||||
if (usedCustomStrategies.length > 0) {
|
||||
@ -89,6 +90,13 @@ export class ImportValidationMessages {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export type IFlagKey =
|
||||
| 'segmentChangeRequests'
|
||||
| 'changeRequestReject'
|
||||
| 'customRootRolesKillSwitch'
|
||||
| 'newApplicationList'
|
||||
| 'integrationsRework';
|
||||
|
||||
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,
|
||||
lastSeenByEnvironment: true,
|
||||
segmentChangeRequests: true,
|
||||
newApplicationList: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
@ -193,7 +193,7 @@ test('all tags are listed in the root "tags" list', async () => {
|
||||
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
|
||||
.get('/docs/openapi.json')
|
||||
.expect('Content-Type', /json/)
|
||||
@ -203,8 +203,8 @@ test('all API operations have summaries and descriptions', async () => {
|
||||
return Object.entries(data)
|
||||
.map(([verb, operationDescription]) => {
|
||||
if (
|
||||
'summary' in operationDescription &&
|
||||
'description' in operationDescription
|
||||
operationDescription.summary &&
|
||||
operationDescription.description
|
||||
) {
|
||||
return undefined;
|
||||
} else {
|
||||
|
@ -8,13 +8,21 @@ This website is built using [Docusaurus 2](https://docusaurus.io/), a modern sta
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Generate Open API docs
|
||||
|
||||
```console
|
||||
yarn generate
|
||||
```
|
||||
|
||||
Generate the Open API docs that live at Reference documentation > APIs > OpenAPI
|
||||
|
||||
## Local Development
|
||||
|
||||
```console
|
||||
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
|
||||
|
||||
|
@ -81,7 +81,7 @@
|
||||
"@tsconfig/docusaurus": "2.0.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"enhanced-resolve": "5.15.0",
|
||||
"react-router": "6.14.2",
|
||||
"react-router": "6.15.0",
|
||||
"replace-in-file": "7.0.1",
|
||||
"storybook-addon-root-attribute": "1.0.2",
|
||||
"typescript": "4.8.4"
|
||||
|
@ -3291,10 +3291,10 @@
|
||||
redux-thunk "^2.4.2"
|
||||
reselect "^4.1.7"
|
||||
|
||||
"@remix-run/router@1.7.2":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
|
||||
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
|
||||
"@remix-run/router@1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||
|
||||
"@sideway/address@^4.1.3":
|
||||
version "4.1.4"
|
||||
@ -13714,12 +13714,12 @@ react-router@5.3.4, react-router@^5.3.3:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@6.14.2:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300"
|
||||
integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==
|
||||
react-router@6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.7.2"
|
||||
"@remix-run/router" "1.8.0"
|
||||
|
||||
react-textarea-autosize@^8.3.2:
|
||||
version "8.4.0"
|
||||
|
Loading…
Reference in New Issue
Block a user