diff --git a/.node-version b/.node-version index 603606bc91..4a1f488b6c 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.17.0 +18.17.1 diff --git a/frontend/package.json b/frontend/package.json index 0a99c04f6f..93cc482ee2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/component/admin/adminRoutes.ts b/frontend/src/component/admin/adminRoutes.ts index ff5e7916ed..eed5467cd1 100644 --- a/frontend/src/component/admin/adminRoutes.ts +++ b/frontend/src/component/admin/adminRoutes.ts @@ -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', }, { diff --git a/frontend/src/component/admin/invoice/InvoiceList.tsx b/frontend/src/component/admin/invoice/InvoiceList.tsx index 5e14509599..a690f43536 100644 --- a/frontend/src/component/admin/invoice/InvoiceList.tsx +++ b/frontend/src/component/admin/invoice/InvoiceList.tsx @@ -115,7 +115,9 @@ const InvoiceList = () => { } - elseShow={
{isLoaded && 'No invoices to show.'}
} + elseShow={ + {isLoaded && 'No invoices to show.'} + } /> ); }; diff --git a/frontend/src/component/admin/menu/AdminTabsMenu.tsx b/frontend/src/component/admin/menu/AdminTabsMenu.tsx index 685f4f76a2..81c758984b 100644 --- a/frontend/src/component/admin/menu/AdminTabsMenu.tsx +++ b/frontend/src/component/admin/menu/AdminTabsMenu.tsx @@ -55,6 +55,7 @@ export const AdminTabsMenu: VFC = () => { > {tabs.map(tab => ( { - 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 ( - createNavLinkStyle({ isActive, theme })} - > - {children} - - ); + return {children}; }; diff --git a/frontend/src/component/admin/network/Network.tsx b/frontend/src/component/admin/network/Network.tsx index a6fa643b24..33d32d6216 100644 --- a/frontend/src/component/admin/network/Network.tsx +++ b/frontend/src/component/admin/network/Network.tsx @@ -43,6 +43,7 @@ export const Network = () => { {label} } + sx={{ padding: 0 }} /> ))} diff --git a/frontend/src/component/admin/roles/RolesPage.tsx b/frontend/src/component/admin/roles/RolesPage.tsx index e708b02e98..681ae9e6d5 100644 --- a/frontend/src/component/admin/roles/RolesPage.tsx +++ b/frontend/src/component/admin/roles/RolesPage.tsx @@ -97,6 +97,7 @@ export const RolesPage = () => { } + sx={{ padding: 0 }} /> ))} diff --git a/frontend/src/component/admin/useAdminRoutes.ts b/frontend/src/component/admin/useAdminRoutes.ts index 61b85f11de..1e4b529f86 100644 --- a/frontend/src/component/admin/useAdminRoutes.ts +++ b/frontend/src/component/admin/useAdminRoutes.ts @@ -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( diff --git a/frontend/src/component/application/ApplicationList/ApplicationList.tsx b/frontend/src/component/application/ApplicationList/ApplicationList.tsx index 19d31c7f86..d324e910c3 100644 --- a/frontend/src/component/application/ApplicationList/ApplicationList.tsx +++ b/frontend/src/component/application/ApplicationList/ApplicationList.tsx @@ -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>; +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) => ( + + {icon} + + } + /> + ), + disableGlobalFilter: true, + }, + { + Header: 'Name', + accessor: 'appName', + width: '50%', + Cell: ({ + row: { + original: { appName, description }, + }, + }: any) => ( + + ), + sortType: 'alphanumeric', + }, + { + Header: 'Project(environment)', + accessor: 'usage', + width: '50%', + Cell: () => ( + + + not connected + + + ), + 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 ; } - let applicationCount = - filteredApplications.length < applications.length - ? `${filteredApplications.length} of ${applications.length}` - : applications.length; - return ( <> } /> @@ -82,8 +168,37 @@ export const ApplicationList = () => { >
0} - show={} + condition={data.length > 0} + show={ + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render( + 'Cell' + )} + + ))} + + ); + })} + +
+
+ } elseShow={ >; + +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 = () => ( + <> +
+
+
+ Oh snap, it does not seem like you have connected any + applications. To connect your application to Unleash you will + require a Client SDK. +
+
+ You can read more about how to use Unleash in your application + in the{' '} + + documentation. + +
+ + ); + + if (!filteredApplications) { + return ; + } + + let applicationCount = + filteredApplications.length < applications.length + ? `${filteredApplications.length} of ${applications.length}` + : applications.length; + + return ( + <> + + } + /> + } + > +
+ 0} + show={} + elseShow={ + ...loading
} + elseShow={renderNoApplications()} + /> + } + /> +
+
+ + ); +}; diff --git a/frontend/src/component/application/ApplicationList/TemporaryApplicationListWrapper.tsx b/frontend/src/component/application/ApplicationList/TemporaryApplicationListWrapper.tsx new file mode 100644 index 0000000000..120bd5e536 --- /dev/null +++ b/frontend/src/component/application/ApplicationList/TemporaryApplicationListWrapper.tsx @@ -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 ( + } + elseShow={} + /> + ); +}; diff --git a/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx b/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx index da60c200cb..56b538a987 100644 --- a/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx +++ b/frontend/src/component/menu/Header/NavigationMenu/NavigationMenu.tsx @@ -98,9 +98,9 @@ export const NavigationMenu = ({ } arrow placement="left" + key={option.path} > =3.0.0 <4.0.0" immutable "^4.0.0" diff --git a/src/lib/addons/slack-app-definition.ts b/src/lib/addons/slack-app-definition.ts index 5df01d5c7b..fb2d5eb68d 100644 --- a/src/lib/addons/slack-app-definition.ts +++ b/src/lib/addons/slack-app-definition.ts @@ -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, diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts index bdf61c91d0..47f901de8c 100644 --- a/src/lib/addons/slack-app.ts +++ b/src/lib/addons/slack-app.ts @@ -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 { 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( diff --git a/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts b/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts index cbd114f258..bd868722f1 100644 --- a/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts +++ b/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts @@ -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: [ { diff --git a/src/lib/features/export-import-toggles/export-import-service.ts b/src/lib/features/export-import-toggles/export-import-service.ts index 3c8c8252ab..3e553acefc 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -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, diff --git a/src/lib/features/export-import-toggles/import-toggles-store-type.ts b/src/lib/features/export-import-toggles/import-toggles-store-type.ts index 1277e8e61b..d8c1aabd2a 100644 --- a/src/lib/features/export-import-toggles/import-toggles-store-type.ts +++ b/src/lib/features/export-import-toggles/import-toggles-store-type.ts @@ -11,6 +11,11 @@ export interface IImportTogglesStore { project: string, ): Promise<{ name: string; project: string }[]>; + getFeaturesInProject( + featureNames: string[], + project: string, + ): Promise; + deleteTagsForFeatures(tags: string[]): Promise; strategiesExistForFeatures( diff --git a/src/lib/features/export-import-toggles/import-toggles-store.ts b/src/lib/features/export-import-toggles/import-toggles-store.ts index fe74fa0cff..0daf877622 100644 --- a/src/lib/features/export-import-toggles/import-toggles-store.ts +++ b/src/lib/features/export-import-toggles/import-toggles-store.ts @@ -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 { + 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 { return this.db(T.featureTag).whereIn('feature_name', features).del(); } diff --git a/src/lib/features/export-import-toggles/import-validation-messages.ts b/src/lib/features/export-import-toggles/import-validation-messages.ts index 9017776792..376df05189 100644 --- a/src/lib/features/export-import-toggles/import-validation-messages.ts +++ b/src/lib/features/export-import-toggles/import-validation-messages.ts @@ -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; } } diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 573e69dd52..5163b3614d 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -31,6 +31,7 @@ export type IFlagKey = | 'segmentChangeRequests' | 'changeRequestReject' | 'customRootRolesKillSwitch' + | 'newApplicationList' | 'integrationsRework'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; diff --git a/src/migrations/20230817095805-client-applications-usage-table.js b/src/migrations/20230817095805-client-applications-usage-table.js new file mode 100644 index 0000000000..64b095e3ab --- /dev/null +++ b/src/migrations/20230817095805-client-applications-usage-table.js @@ -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, + ); +}; diff --git a/src/server-dev.ts b/src/server-dev.ts index fe4fee7c64..769c110eb8 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -45,6 +45,7 @@ process.nextTick(async () => { frontendNavigationUpdate: true, lastSeenByEnvironment: true, segmentChangeRequests: true, + newApplicationList: true, }, }, authentication: { diff --git a/src/test/e2e/api/openapi/openapi.e2e.test.ts b/src/test/e2e/api/openapi/openapi.e2e.test.ts index 9477e7fffb..29ae6bbc00 100644 --- a/src/test/e2e/api/openapi/openapi.e2e.test.ts +++ b/src/test/e2e/api/openapi/openapi.e2e.test.ts @@ -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 { diff --git a/website/README.md b/website/README.md index 231a499c0d..7eee45919d 100644 --- a/website/README.md +++ b/website/README.md @@ -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 diff --git a/website/package.json b/website/package.json index 23bfb7754c..2deaaa84fd 100644 --- a/website/package.json +++ b/website/package.json @@ -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" diff --git a/website/yarn.lock b/website/yarn.lock index 62bc8d8a1a..cf3d0628fd 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -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"