diff --git a/.changeset/cli-pack-so-exhausting.md b/.changeset/cli-pack-so-exhausting.md new file mode 100644 index 0000000000..e4f92521d0 --- /dev/null +++ b/.changeset/cli-pack-so-exhausting.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed a bug that could cause the `build-workspace` command to fail when invoked with `--alwaysYarnPack` enabled in environments with limited resources. diff --git a/.changeset/dry-bananas-ring.md b/.changeset/dry-bananas-ring.md new file mode 100644 index 0000000000..e37ea2e5c3 --- /dev/null +++ b/.changeset/dry-bananas-ring.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +the user can newly mark notifications as "Saved" for their better visibility in the future diff --git a/.changeset/hot-moles-lay.md b/.changeset/hot-moles-lay.md new file mode 100644 index 0000000000..90adcbd07b --- /dev/null +++ b/.changeset/hot-moles-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-airbrake': patch +--- + +added an optional ESLint rule - no-top-level-material-ui-4-imports - which has an auto fix function to migrate the imports and using it migrated the imports diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 02d4d55301..638f5213be 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -29,7 +29,7 @@ jobs: cache-prefix: ${{ runner.os }}-v18.x - name: Create Snyk report - uses: snyk/actions/node@1d672a455ab3339ef0a0021e1ec809165ee12fad # master + uses: snyk/actions/node@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # master continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found with: args: > diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index c5374951d7..1ffc8d2c26 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Monitor and Synchronize Snyk Policies - uses: snyk/actions/node@1d672a455ab3339ef0a0021e1ec809165ee12fad # master + uses: snyk/actions/node@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # master with: command: monitor args: > @@ -46,7 +46,7 @@ jobs: # Above we run the `monitor` command, this runs the `test` command which is # the one that generates the SARIF report that we can upload to GitHub. - name: Create Snyk report - uses: snyk/actions/node@1d672a455ab3339ef0a0021e1ec809165ee12fad # master + uses: snyk/actions/node@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # master continue-on-error: true # To make sure that SARIF upload gets called with: args: > diff --git a/docs/features/software-templates/authorizing-parameters-steps-and-actions.md b/docs/features/software-templates/authorizing-parameters-steps-and-actions.md index 27e8d48023..0ecde1c57e 100644 --- a/docs/features/software-templates/authorizing-parameters-steps-and-actions.md +++ b/docs/features/software-templates/authorizing-parameters-steps-and-actions.md @@ -15,6 +15,8 @@ apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: my_custom_template +spec: + type: service parameters: - title: Provide some simple information properties: diff --git a/packages/cli/package.json b/packages/cli/package.json index 910769ff06..163c204ec7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -112,6 +112,7 @@ "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", + "p-limit": "^3.1.0", "p-queue": "^6.6.2", "postcss": "^8.1.0", "process": "^0.11.10", diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index 00b8784d6d..9cb54f2830 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -21,6 +21,7 @@ import { resolve as resolvePath, relative as relativePath, } from 'path'; +import pLimit from 'p-limit'; import { tmpdir } from 'os'; import tar, { CreateOptions, FileOptions } from 'tar'; import partition from 'lodash/partition'; @@ -352,9 +353,11 @@ async function moveToDistWorkspace( } // Repacking in parallel is much faster and safe for all packages outside of the Backstage repo + // Limit concurrency to 10 to avoid resource exhaustion on larger monorepos. + const limit = pLimit(10); await Promise.all( - safePackages.map(async (target, index) => - pack(target, `temp-package-${index}.tgz`), + safePackages.map((target, index) => + limit(() => pack(target, `temp-package-${index}.tgz`)), ), ); } diff --git a/plugins/airbrake/.eslintrc.js b/plugins/airbrake/.eslintrc.js index e2a53a6ad2..9d5b45a010 100644 --- a/plugins/airbrake/.eslintrc.js +++ b/plugins/airbrake/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-material-ui-4-imports': 'error', + }, + }); \ No newline at end of file diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index 7c79dc8363..f75bc4160f 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { makeStyles, TextField } from '@material-ui/core'; +import TextField from '@material-ui/core/TextField'; +import { makeStyles } from '@material-ui/core/styles'; import { Context } from '../ContextProvider'; const useStyles = makeStyles(theme => ({ diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 666a7ffea2..f63c0db86b 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -22,7 +22,8 @@ import { Progress, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { Grid, Typography } from '@material-ui/core'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index d17665fbf6..51dcff07af 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -224,6 +224,12 @@ export async function createRouter( opts.read = false; // or keep undefined } + if (req.query.saved === 'true') { + opts.saved = true; + } else if (req.query.saved === 'false') { + opts.saved = false; + // or keep undefined + } if (req.query.created_after) { const sinceEpoch = Date.parse(String(req.query.created_after)); if (isNaN(sinceEpoch)) { diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 70ebbd8bfd..be57d9da73 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -22,6 +22,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + saved?: boolean; createdAfter?: Date; sort?: 'created' | 'topic' | 'origin'; sortOrder?: 'asc' | 'desc'; diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 9820373691..8e80f5908c 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -30,6 +30,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + saved?: boolean; createdAfter?: Date; sort?: 'created' | 'topic' | 'origin'; sortOrder?: 'asc' | 'desc'; diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 528325a957..d98e0080e8 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -61,6 +61,9 @@ export class NotificationsClient implements NotificationsApi { if (options?.read !== undefined) { queryString.append('read', options.read ? 'true' : 'false'); } + if (options?.saved !== undefined) { + queryString.append('saved', options.saved ? 'true' : 'false'); + } if (options?.createdAfter !== undefined) { queryString.append('created_after', options.createdAfter.toISOString()); } diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index a44a58fe32..ad42c03d16 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -37,6 +37,8 @@ export type NotificationsFiltersProps = { onCreatedAfterChanged: (value: string) => void; sorting: SortBy; onSortingChanged: (sortBy: SortBy) => void; + saved?: boolean; + onSavedChanged: (checked: boolean | undefined) => void; }; export const CreatedAfterOptions: { @@ -113,6 +115,8 @@ export const NotificationsFilters = ({ onUnreadOnlyChanged, createdAfter, onCreatedAfterChanged, + saved, + onSavedChanged, }: NotificationsFiltersProps) => { const sortByText = getSortByText(sorting); @@ -122,13 +126,23 @@ export const NotificationsFilters = ({ onCreatedAfterChanged(event.target.value as string); }; - const handleOnUnreadOnlyChanged = ( + const handleOnViewChanged = ( event: React.ChangeEvent<{ name?: string; value: unknown }>, ) => { - let value = undefined; - if (event.target.value === 'unread') value = true; - if (event.target.value === 'read') value = false; - onUnreadOnlyChanged(value); + if (event.target.value === 'unread') { + onUnreadOnlyChanged(true); + onSavedChanged(undefined); + } else if (event.target.value === 'read') { + onUnreadOnlyChanged(false); + onSavedChanged(undefined); + } else if (event.target.value === 'saved') { + onUnreadOnlyChanged(undefined); + onSavedChanged(true); + } else { + // All + onUnreadOnlyChanged(undefined); + onSavedChanged(undefined); + } }; const handleOnSortByChanged = ( @@ -139,9 +153,14 @@ export const NotificationsFilters = ({ onSortingChanged({ ...option.sortBy }); }; - let unreadOnlyValue = 'all'; - if (unreadOnly) unreadOnlyValue = 'unread'; - if (unreadOnly === false) unreadOnlyValue = 'read'; + let viewValue = 'all'; + if (saved) { + viewValue = 'saved'; + } else if (unreadOnly) { + viewValue = 'unread'; + } else if (unreadOnly === false) { + viewValue = 'read'; + } return ( <> @@ -156,10 +175,11 @@ export const NotificationsFilters = ({ diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 6b3292b892..54acc5d7bc 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -37,6 +37,7 @@ export const NotificationsPage = () => { const [refresh, setRefresh] = React.useState(false); const { lastSignal } = useSignal('notifications'); const [unreadOnly, setUnreadOnly] = React.useState(true); + const [saved, setSaved] = React.useState(undefined); const [pageNumber, setPageNumber] = React.useState(0); const [pageSize, setPageSize] = React.useState(5); const [containsText, setContainsText] = React.useState(); @@ -56,6 +57,9 @@ export const NotificationsPage = () => { if (unreadOnly !== undefined) { options.read = !unreadOnly; } + if (saved !== undefined) { + options.saved = saved; + } const createdAfterDate = CreatedAfterOptions[createdAfter].getDate(); if (createdAfterDate.valueOf() > 0) { @@ -64,7 +68,15 @@ export const NotificationsPage = () => { return api.getNotifications(options); }, - [containsText, unreadOnly, createdAfter, pageNumber, pageSize, sorting], + [ + containsText, + unreadOnly, + createdAfter, + pageNumber, + pageSize, + sorting, + saved, + ], ); useEffect(() => { @@ -100,6 +112,8 @@ export const NotificationsPage = () => { onCreatedAfterChanged={setCreatedAfter} onSortingChanged={setSorting} sorting={sorting} + saved={saved} + onSavedChanged={setSaved} /> diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 3452a618fb..acbeef8769 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -17,13 +17,11 @@ import React, { useMemo } from 'react'; import throttle from 'lodash/throttle'; // @ts-ignore import RelativeTime from 'react-relative-time'; -import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Box, Grid, IconButton, Tooltip, Typography } from '@material-ui/core'; import { Notification } from '@backstage/plugin-notifications-common'; import { notificationsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; -import MarkAsUnreadIcon from '@material-ui/icons/Markunread'; -import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; import { Link, Table, @@ -31,6 +29,11 @@ import { TableColumn, } from '@backstage/core-components'; +import MarkAsUnreadIcon from '@material-ui/icons/Markunread' /* TODO: use Drafts and MarkAsUnread once we have mui 5 icons */; +import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; +import MarkAsUnsavedIcon from '@material-ui/icons/LabelOff' /* TODO: use BookmarkRemove and BookmarkAdd once we have mui 5 icons */; +import MarkAsSavedIcon from '@material-ui/icons/Label'; + const ThrottleDelayMs = 1000; /** @public */ @@ -71,6 +74,18 @@ export const NotificationsTable = ({ [notificationsApi, onUpdate], ); + const onSwitchSavedStatus = React.useCallback( + (notification: Notification) => { + notificationsApi + .updateNotifications({ + ids: [notification.id], + saved: !notification.saved, + }) + .then(() => onUpdate()); + }, + [notificationsApi, onUpdate], + ); + const throttledContainsTextHandler = useMemo( () => throttle(setContainsText, ThrottleDelayMs), [setContainsText], @@ -136,7 +151,6 @@ export const NotificationsTable = ({ // }, // }, { - // TODO: action for saving notifications // actions width: '1rem', render: (notification: Notification) => { @@ -147,24 +161,47 @@ export const NotificationsTable = ({ ? MarkAsUnreadIcon : MarkAsReadIcon; + const markAsSavedText = !!notification.saved + ? 'Undo save' + : 'Save for later'; + + const SavedIconComponent = !!notification.saved + ? MarkAsUnsavedIcon + : MarkAsSavedIcon; + return ( - - { - onSwitchReadStatus(notification); - }} - > - - - + + + + { + onSwitchSavedStatus(notification); + }} + > + + + + + + + + { + onSwitchReadStatus(notification); + }} + > + + + + + ); }, }, ], - [onSwitchReadStatus], + [onSwitchReadStatus, onSwitchSavedStatus], ); - // TODO: render "Saved notifications" as "Pinned" return ( isLoading={isLoading} diff --git a/yarn.lock b/yarn.lock index 0f498305c3..a0d94a3027 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3691,6 +3691,7 @@ __metadata: nodemon: ^3.0.1 npm-packlist: ^5.0.0 ora: ^5.3.0 + p-limit: ^3.1.0 p-queue: ^6.6.2 postcss: ^8.1.0 process: ^0.11.10 @@ -18735,12 +18736,12 @@ __metadata: linkType: hard "@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.21": - version: 3.3.24 - resolution: "@types/dockerode@npm:3.3.24" + version: 3.3.26 + resolution: "@types/dockerode@npm:3.3.26" dependencies: "@types/docker-modem": "*" "@types/node": "*" - checksum: 00329ba9225f5b57bfc0ba8c4dddb17100ebe13c5192fe8a14fce59eec456d258e814b6c78df26d7d7bb878fc38455f559f278490c9c8efad8f708335643b40e + checksum: ec1e83ef2d938813c8fa72f7c8744a9bf598369d8a0f340377ddff25c6e7d118379cd297eecbe4af2b7aa64bf5cd1bb66b32d1e3874a062433b967265be94f09 languageName: node linkType: hard