diff --git a/app/azure/AzureSidebar/AzureSidebar.test.tsx b/app/azure/AzureSidebar/AzureSidebar.test.tsx
new file mode 100644
index 000000000..607e6663b
--- /dev/null
+++ b/app/azure/AzureSidebar/AzureSidebar.test.tsx
@@ -0,0 +1,34 @@
+import { render, within } from '@/react-tools/test-utils';
+
+import { AzureSidebar } from './AzureSidebar';
+
+test('dashboard items should render correctly', () => {
+ const { getByLabelText } = renderComponent();
+ const dashboardItem = getByLabelText('Dashboard');
+ expect(dashboardItem).toBeVisible();
+ expect(dashboardItem).toHaveTextContent('Dashboard');
+
+ const dashboardItemElements = within(dashboardItem);
+ expect(dashboardItemElements.getByLabelText('itemIcon')).toBeVisible();
+ expect(dashboardItemElements.getByLabelText('itemIcon')).toHaveClass(
+ 'fa-tachometer-alt',
+ 'fa-fw'
+ );
+
+ const containerInstancesItem = getByLabelText('ContainerInstances');
+ expect(containerInstancesItem).toBeVisible();
+ expect(containerInstancesItem).toHaveTextContent('Container instances');
+
+ const containerInstancesItemElements = within(containerInstancesItem);
+ expect(
+ containerInstancesItemElements.getByLabelText('itemIcon')
+ ).toBeVisible();
+ expect(containerInstancesItemElements.getByLabelText('itemIcon')).toHaveClass(
+ 'fa-cubes',
+ 'fa-fw'
+ );
+});
+
+function renderComponent() {
+ return render();
+}
diff --git a/app/azure/AzureSidebar/AzureSidebar.tsx b/app/azure/AzureSidebar/AzureSidebar.tsx
new file mode 100644
index 000000000..ca83d51b5
--- /dev/null
+++ b/app/azure/AzureSidebar/AzureSidebar.tsx
@@ -0,0 +1,36 @@
+import { r2a } from '@/react-tools/react2angular';
+import { SidebarMenuItem } from '@/portainer/components/sidebar/SidebarMenuItem';
+import type { EnvironmentId } from '@/portainer/environments/types';
+
+interface Props {
+ environmentId: EnvironmentId;
+}
+
+export function AzureSidebar({ environmentId }: Props) {
+ return (
+ <>
+
+ Dashboard
+
+
+ Container instances
+
+ >
+ );
+}
+
+export const AzureSidebarAngular = r2a(AzureSidebar, ['environmentId']);
diff --git a/app/azure/AzureSidebar/index.ts b/app/azure/AzureSidebar/index.ts
new file mode 100644
index 000000000..23b4fe95c
--- /dev/null
+++ b/app/azure/AzureSidebar/index.ts
@@ -0,0 +1 @@
+export { AzureSidebar, AzureSidebarAngular } from './AzureSidebar';
diff --git a/app/azure/_module.js b/app/azure/_module.js
index 7cd0c77ee..aaf5b78f1 100644
--- a/app/azure/_module.js
+++ b/app/azure/_module.js
@@ -1,5 +1,6 @@
import angular from 'angular';
+import { AzureSidebarAngular } from './AzureSidebar/AzureSidebar';
import { DashboardViewAngular } from './Dashboard/DashboardView';
import { containerInstancesModule } from './ContainerInstances';
@@ -82,4 +83,5 @@ angular
$stateRegistryProvider.register(dashboard);
},
])
- .component('dashboardView', DashboardViewAngular).name;
+ .component('azureSidebar', AzureSidebarAngular)
+ .component('dashboardView', DashboardViewAngular);
diff --git a/app/azure/components/azure-sidebar/azure-sidebar.html b/app/azure/components/azure-sidebar/azure-sidebar.html
deleted file mode 100644
index 08fcec28e..000000000
--- a/app/azure/components/azure-sidebar/azure-sidebar.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
- Dashboard
-
-
-
- Container instances
-
diff --git a/app/azure/components/azure-sidebar/index.js b/app/azure/components/azure-sidebar/index.js
deleted file mode 100644
index 5b5b13f8b..000000000
--- a/app/azure/components/azure-sidebar/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import angular from 'angular';
-
-angular.module('portainer.azure').component('azureSidebar', {
- templateUrl: './azure-sidebar.html',
- bindings: {
- endpointId: '<',
- },
-});
diff --git a/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.module.css b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.module.css
new file mode 100644
index 000000000..4f751ad36
--- /dev/null
+++ b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.module.css
@@ -0,0 +1,4 @@
+.sidebar-menu-item > a {
+ display: flex;
+ justify-content: space-between;
+}
diff --git a/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.stories.tsx b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.stories.tsx
new file mode 100644
index 000000000..aae4d9843
--- /dev/null
+++ b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.stories.tsx
@@ -0,0 +1,49 @@
+import { Meta, Story } from '@storybook/react';
+
+import { SidebarMenuItem } from './SidebarMenuItem';
+
+const meta: Meta = {
+ title: 'Components/SidebarMenuItem',
+ component: SidebarMenuItem,
+};
+export default meta;
+
+interface StoryProps {
+ iconClass?: string;
+ className: string;
+ itemName: string;
+ linkName: string;
+}
+
+function Template({ iconClass, className, itemName, linkName }: StoryProps) {
+ return (
+
+ );
+}
+
+export const Primary: Story = Template.bind({});
+Primary.args = {
+ iconClass: 'fa-tachometer-alt fa-fw',
+ className: 'exampleItemClass',
+ itemName: 'ExampleItem',
+ linkName: 'Item with icon',
+};
+
+export const WithoutIcon: Story = Template.bind({});
+WithoutIcon.args = {
+ className: 'exampleItemClass',
+ itemName: 'ExampleItem',
+ linkName: 'Item without icon',
+};
diff --git a/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.test.tsx b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.test.tsx
new file mode 100644
index 000000000..febee07bf
--- /dev/null
+++ b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.test.tsx
@@ -0,0 +1,51 @@
+import { render } from '@/react-tools/test-utils';
+
+import { SidebarMenuItem } from './SidebarMenuItem';
+
+test('should be visible & have expected class', () => {
+ const { getByLabelText } = renderComponent('testClass');
+ const listItem = getByLabelText('sidebarItem');
+ expect(listItem).toBeVisible();
+ expect(listItem).toHaveClass('testClass');
+});
+
+test('icon should with correct icon if iconClass is provided', () => {
+ const { getByLabelText } = renderComponent('', 'testIconClass');
+ const sidebarIcon = getByLabelText('itemIcon');
+ expect(sidebarIcon).toBeVisible();
+ expect(sidebarIcon).toHaveClass('testIconClass');
+});
+
+test('icon should not be rendered if iconClass is not provided', () => {
+ const { queryByLabelText } = renderComponent();
+ expect(queryByLabelText('itemIcon')).not.toBeInTheDocument();
+});
+
+test('should render children', () => {
+ const { getByLabelText } = renderComponent('', '', 'Test');
+ expect(getByLabelText('sidebarItem')).toHaveTextContent('Test');
+});
+
+test('li element should have correct accessibility label', () => {
+ const { queryByLabelText } = renderComponent('', '', '', 'testItemLabel');
+ expect(queryByLabelText('testItemLabel')).toBeInTheDocument();
+});
+
+function renderComponent(
+ className = '',
+ iconClass = '',
+ linkText = '',
+ itemName = 'sidebarItem'
+) {
+ return render(
+
+ {linkText}
+
+ );
+}
diff --git a/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.tsx b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.tsx
new file mode 100644
index 000000000..f196da7ac
--- /dev/null
+++ b/app/portainer/components/sidebar/SidebarMenuItem/SidebarMenuItem.tsx
@@ -0,0 +1,44 @@
+import { PropsWithChildren } from 'react';
+import clsx from 'clsx';
+import { UISrefActive } from '@uirouter/react';
+
+import { Link } from '@/portainer/components/Link';
+
+import '../sidebar.css';
+import styles from './SidebarMenuItem.module.css';
+
+interface Props {
+ path: string;
+ pathParams: object;
+ iconClass?: string;
+ className: string;
+ itemName: string;
+}
+
+export function SidebarMenuItem({
+ path,
+ pathParams,
+ iconClass,
+ className,
+ itemName,
+ children,
+}: PropsWithChildren) {
+ return (
+
+
+
+ {children}
+ {iconClass && (
+
+ )}
+
+
+
+ );
+}
diff --git a/app/portainer/components/sidebar/SidebarMenuItem/index.ts b/app/portainer/components/sidebar/SidebarMenuItem/index.ts
new file mode 100644
index 000000000..a8bb88d26
--- /dev/null
+++ b/app/portainer/components/sidebar/SidebarMenuItem/index.ts
@@ -0,0 +1 @@
+export { SidebarMenuItem } from './SidebarMenuItem';
diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html
index 46af92d9a..a5198c0d4 100644
--- a/app/portainer/views/sidebar/sidebar.html
+++ b/app/portainer/views/sidebar/sidebar.html
@@ -23,7 +23,7 @@
admin-access="isAdmin"
>
-
+