Skip to content

Commit fb4ae44

Browse files
authored
[announcements] Admin categories and tags rewrite with @backstage/ui for NFS (#6484)
1 parent 6efdc5b commit fb4ae44

File tree

23 files changed

+1198
-31
lines changed

23 files changed

+1198
-31
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage-community/plugin-announcements': patch
3+
---
4+
5+
The category and tag content tabs in the admin portal have been rewritten with `@backstage/ui` for users on the new frontend system. Users on the existing frontend system will not see any changes.

workspaces/announcements/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"prettier:check": "prettier --check .",
2828
"prettier:fix": "prettier --ignore-unknown --write .",
2929
"new": "backstage-cli new --scope @backstage-community",
30-
"validate": "yarn build:all && yarn lint && yarn prettier:fix && yarn build:list-deprecations"
30+
"validate": "yarn && yarn tsc && yarn lint && yarn prettier:fix && yarn build:list-deprecations && yarn build:api-reports"
3131
},
3232
"repository": {
3333
"type": "git",

workspaces/announcements/plugins/announcements/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,13 @@
6464
"@backstage/plugin-search-react": "^1.10.1",
6565
"@backstage/plugin-signals-react": "^0.0.18",
6666
"@backstage/theme": "^0.7.1",
67-
"@backstage/ui": "^0.9.1",
67+
"@backstage/ui": "^0.10.0",
6868
"@material-ui/core": "^4.12.2",
6969
"@material-ui/icons": "^4.11.3",
7070
"@material-ui/lab": "4.0.0-alpha.61",
7171
"@mui/icons-material": "^5.15.6",
7272
"@mui/material": "^5.15.6",
73+
"@remixicon/react": "^4.7.0",
7374
"@types/react": "^17.0.0 || ^18.0.0",
7475
"@uiw/react-md-editor": "^4.0.8",
7576
"add": "^2.0.6",

workspaces/announcements/plugins/announcements/src/alpha/Router.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@
1616
import { Routes, Route } from 'react-router-dom';
1717
import { RequirePermission } from '@backstage/plugin-permission-react';
1818
import { announcementCreatePermission } from '@backstage-community/plugin-announcements-common';
19-
import { AnnouncementsAdminPage } from './components/admin/AnnouncementsAdminPage';
19+
import {
20+
AnnouncementsAdminPage,
21+
CategoriesContent,
22+
TagsContent,
23+
} from './components';
2024
import { AnnouncementsContent, MarkdownRendererTypeProps } from '../components';
2125
import {
2226
AnnouncementsPage,
2327
AnnouncementsPageProps,
2428
} from '../components/AnnouncementsPage';
2529
import { AnnouncementPage } from '../components/AnnouncementPage';
26-
import { CategoriesContent } from '../components/Admin/CategoriesContent';
27-
import { TagsContent } from '../components/Admin/TagsContent';
2830

2931
type RouterProps = {
3032
themeId?: string;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2024 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { useState } from 'react';
17+
import { useApi, alertApiRef } from '@backstage/core-plugin-api';
18+
import { ResponseError } from '@backstage/errors';
19+
import {
20+
CreateCategoryRequest,
21+
announcementsApiRef,
22+
useAnnouncementsTranslation,
23+
useCategories,
24+
useAnnouncementsPermissions,
25+
} from '@backstage-community/plugin-announcements-react';
26+
import { Category } from '@backstage-community/plugin-announcements-common';
27+
28+
import {
29+
useDeleteConfirmationDialogState,
30+
DeleteConfirmationDialog,
31+
} from '../shared';
32+
import { CreateCatagoryDialog } from './CreateCatagoryDialog';
33+
import { CategoriesTableCard } from './CategoriesTableCard';
34+
35+
/**
36+
* @internal
37+
*/
38+
export const CategoriesContent = () => {
39+
const announcementsApi = useApi(announcementsApiRef);
40+
const alertApi = useApi(alertApiRef);
41+
42+
const [showNewCategoryForm, setShowNewCategoryForm] = useState(false);
43+
const { t } = useAnnouncementsTranslation();
44+
const permissions = useAnnouncementsPermissions();
45+
46+
const { categories, retry: refresh } = useCategories();
47+
48+
const {
49+
isOpen: isDeleteDialogOpen,
50+
close: closeDeleteDialog,
51+
open: openDeleteDialog,
52+
item: categoryToDelete,
53+
} = useDeleteConfirmationDialogState<Category>();
54+
55+
const onConfirmCreate = async (request: CreateCategoryRequest) => {
56+
const { title } = request;
57+
58+
try {
59+
await announcementsApi.createCategory({
60+
title,
61+
});
62+
63+
alertApi.post({
64+
message: `${title} ${t('admin.categoriesContent.createdMessage')}`,
65+
severity: 'success',
66+
});
67+
68+
setShowNewCategoryForm(false);
69+
refresh();
70+
} catch (err) {
71+
alertApi.post({ message: (err as Error).message, severity: 'error' });
72+
}
73+
};
74+
75+
const onCreateButtonClick = () => {
76+
setShowNewCategoryForm(true);
77+
};
78+
79+
const onCancelCreate = () => {
80+
setShowNewCategoryForm(false);
81+
};
82+
83+
const onCancelDelete = () => {
84+
closeDeleteDialog();
85+
};
86+
87+
const onConfirmDelete = async () => {
88+
closeDeleteDialog();
89+
90+
try {
91+
await announcementsApi.deleteCategory(categoryToDelete!.slug);
92+
93+
alertApi.post({
94+
message: t('admin.categoriesContent.table.categoryDeleted'),
95+
severity: 'success',
96+
});
97+
} catch (err) {
98+
alertApi.post({
99+
message: (err as ResponseError).body.error.message,
100+
severity: 'error',
101+
});
102+
}
103+
104+
refresh();
105+
};
106+
107+
const onDeleteClick = (category: Category) => {
108+
openDeleteDialog(category);
109+
};
110+
111+
const canCreate = !permissions.create.loading && permissions.create.allowed;
112+
const canDelete = !permissions.delete.loading && permissions.delete.allowed;
113+
114+
return (
115+
<>
116+
<CategoriesTableCard
117+
categories={categories ?? []}
118+
onCreateClick={onCreateButtonClick}
119+
onDeleteClick={onDeleteClick}
120+
canCreate={canCreate}
121+
canDelete={canDelete}
122+
/>
123+
124+
<CreateCatagoryDialog
125+
open={showNewCategoryForm}
126+
onConfirm={onConfirmCreate}
127+
onCancel={onCancelCreate}
128+
canSubmit={canCreate}
129+
/>
130+
131+
<DeleteConfirmationDialog
132+
type="category"
133+
itemTitle={categoryToDelete?.title}
134+
open={isDeleteDialogOpen}
135+
onCancel={onCancelDelete}
136+
onConfirm={onConfirmDelete}
137+
canDelete={canDelete}
138+
/>
139+
</>
140+
);
141+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2025 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import {
17+
CellText,
18+
Column,
19+
Row,
20+
Table,
21+
TableBody,
22+
TableHeader,
23+
Cell,
24+
ButtonIcon,
25+
} from '@backstage/ui';
26+
import { RiDeleteBinLine } from '@remixicon/react';
27+
28+
import { Category } from '@backstage-community/plugin-announcements-common';
29+
import { useAnnouncementsTranslation } from '@backstage-community/plugin-announcements-react';
30+
31+
const CategoriesTableEmptyState = () => {
32+
const { t } = useAnnouncementsTranslation();
33+
34+
return (
35+
<Row>
36+
<CellText
37+
colSpan={3}
38+
title={t('admin.categoriesContent.table.noCategoriesFound')}
39+
/>
40+
</Row>
41+
);
42+
};
43+
44+
type CategoryTableRowProps = {
45+
category: Category;
46+
onDeleteClick?: (category: Category) => void;
47+
};
48+
49+
const CategoryTableRow = (props: CategoryTableRowProps) => {
50+
const { category, onDeleteClick } = props;
51+
52+
return (
53+
<Row key={category.slug}>
54+
<CellText title={category.title} />
55+
<CellText title={category.slug} />
56+
<Cell>
57+
<ButtonIcon
58+
icon={<RiDeleteBinLine />}
59+
variant="tertiary"
60+
onClick={() => onDeleteClick!(category)}
61+
/>
62+
</Cell>
63+
</Row>
64+
);
65+
};
66+
67+
/**
68+
* @internal
69+
*/
70+
type CategoriesTableProps = {
71+
data: Category[];
72+
onDeleteClick?: (category: Category) => void;
73+
};
74+
75+
/**
76+
* @internal
77+
*/
78+
export const CategoriesTable = (props: CategoriesTableProps) => {
79+
const { data, onDeleteClick } = props;
80+
const { t } = useAnnouncementsTranslation();
81+
82+
return (
83+
<Table>
84+
<TableHeader>
85+
<Column id="title" isRowHeader>
86+
{t('admin.categoriesContent.table.title')}
87+
</Column>
88+
<Column id="slug">{t('admin.categoriesContent.table.slug')}</Column>
89+
<Column id="actions">
90+
{t('admin.categoriesContent.table.actions')}
91+
</Column>
92+
</TableHeader>
93+
<TableBody>
94+
{data.length > 0 ? (
95+
data.map(category => (
96+
<CategoryTableRow
97+
key={category.slug}
98+
category={category}
99+
onDeleteClick={onDeleteClick}
100+
/>
101+
))
102+
) : (
103+
<CategoriesTableEmptyState />
104+
)}
105+
</TableBody>
106+
</Table>
107+
);
108+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2024 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { useState, useMemo } from 'react';
17+
import { Category } from '@backstage-community/plugin-announcements-common';
18+
import { useAnnouncementsTranslation } from '@backstage-community/plugin-announcements-react';
19+
import {
20+
Button,
21+
TablePagination,
22+
Card,
23+
CardBody,
24+
CardFooter,
25+
CardHeader,
26+
Text,
27+
Flex,
28+
} from '@backstage/ui';
29+
30+
import { CategoriesTable } from './CategoriesTable';
31+
32+
/**
33+
* @internal
34+
*/
35+
type CategoriesTableCardProps = {
36+
categories: Category[];
37+
onCreateClick: () => void;
38+
onDeleteClick: (category: Category) => void;
39+
canCreate: boolean;
40+
canDelete: boolean;
41+
};
42+
43+
/**
44+
* @internal
45+
*/
46+
export const CategoriesTableCard = (props: CategoriesTableCardProps) => {
47+
const { categories, onCreateClick, onDeleteClick, canCreate, canDelete } =
48+
props;
49+
50+
const [pageSize, setPageSize] = useState(5);
51+
const [offset, setOffset] = useState(0);
52+
const { t } = useAnnouncementsTranslation();
53+
54+
const paginatedCategories = useMemo(() => {
55+
const start = offset;
56+
const end = offset + pageSize;
57+
return categories.slice(start, end);
58+
}, [categories, offset, pageSize]);
59+
60+
const title = `${t('categoriesPage.title')} (${categories.length})`;
61+
62+
return (
63+
<Card>
64+
<CardHeader>
65+
<Flex justify="between" align="center">
66+
<Text variant="title-x-small">{title}</Text>
67+
<Button isDisabled={!canCreate} onClick={onCreateClick}>
68+
{t('admin.categoriesContent.createButton')}
69+
</Button>
70+
</Flex>
71+
</CardHeader>
72+
73+
<CardBody>
74+
<CategoriesTable
75+
data={paginatedCategories}
76+
onDeleteClick={canDelete ? onDeleteClick : undefined}
77+
/>
78+
</CardBody>
79+
80+
{categories.length > 0 && (
81+
<CardFooter>
82+
<TablePagination
83+
offset={offset}
84+
pageSize={pageSize}
85+
setOffset={setOffset}
86+
setPageSize={setPageSize}
87+
rowCount={categories.length}
88+
/>
89+
</CardFooter>
90+
)}
91+
</Card>
92+
);
93+
};

0 commit comments

Comments
 (0)