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" > - +