Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ef5837
refactor(jira-dashboard): migrate MUI primitives to Backstage UI
GabDug Jul 11, 2026
59c8e7f
refactor(jira-dashboard): migrate Tabs, Chip, and LinkButton to Backs…
GabDug Jul 11, 2026
4a6f0ca
refactor(jira-dashboard): migrate InfoCard to BUI EntityInfoCard
GabDug Jul 11, 2026
b111d43
refactor(jira-dashboard): migrate JiraUserIssuesCard to EntityInfoCard
GabDug Jul 11, 2026
8901958
refactor(jira-dashboard): migrate Link to BUI Link
GabDug Jul 11, 2026
f5a64de
style(jira-dashboard): make BUI links standalone (no resting underline)
GabDug Jul 11, 2026
2882324
chore(jira-dashboard): expand dev app to cover all cards and states
GabDug Jul 11, 2026
d47a891
refactor(jira-dashboard): move JiraProjectCard avatar/title into card…
GabDug Jul 11, 2026
61aaf5f
chore(jira-dashboard): drop @mui/material dependency
GabDug Jul 11, 2026
f7b2e40
fix(jira-dashboard): drop title separator when project has no type
GabDug Jul 11, 2026
e620cc5
fix(jira-dashboard): open external Jira links in a new tab
GabDug Jul 11, 2026
be3817d
fix(jira-dashboard): restore JiraProjectCard title weight and avatar …
GabDug Jul 11, 2026
7e5a11d
style(jira-dashboard): match tabbed card header to EntityInfoCard title
GabDug Jul 11, 2026
f14caf5
refactor(jira-dashboard): use EntityInfoCard for the tabbed projects …
GabDug Jul 11, 2026
1a8e1af
fixup: markdown
GabDug Aug 1, 2026
c68f5e0
chore: changeset
GabDug Aug 1, 2026
27ce3c0
fix: use upstream entity title with avatar pattern
GabDug Aug 1, 2026
a290879
fix: Avatar from Jira are 48px, so use BUI 24px small size
GabDug Aug 1, 2026
4423e84
fix: remove jira project title from card when multiple projects (alre…
GabDug Aug 1, 2026
8e16386
chore: bui 0.17
GabDug Aug 4, 2026
fb2db39
fix: key column padding and icon size
GabDug Aug 4, 2026
a3933a5
fix: consistent table title with filters on
GabDug Aug 4, 2026
0d592b2
chore: add BUI styles to app
GabDug Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/tall-pianos-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@axis-backstage/plugin-jira-dashboard': major
---

Migrate Jira Dashboard to Backstage UI. Cards, tabs, links, now use Backstage UI components.

The migration removes the `@mui/material` and `@mui/styles` direct dependencies, but keeping using MUI tables.

**Breaking:** Consumers must install and configure `@backstage/ui`.
1 change: 1 addition & 0 deletions packages/app/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@backstage/cli/asset-types';
import '@backstage/ui/css/styles.css';
import ReactDOM from 'react-dom/client';
import App from './App';

Expand Down
216 changes: 195 additions & 21 deletions plugins/jira-dashboard/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,211 @@
import { ReactNode } from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { EntityJiraDashboardContent, jiraDashboardPlugin } from '../src';
import { jiraDashboardApiRef } from '../src/api';
import { Page, Content } from '@backstage/core-components';
import { JiraDashboardApi, jiraDashboardApiRef } from '../src/api';
import { JiraDashboardContent } from '../src/components/JiraDashboardContent';
import { JiraProjectCard } from '../src/components/JiraProjectCard';
import { JiraTable } from '../src/components/JiraTable';
import { JiraUserIssuesCard } from '../src/components/JiraUserIssuesCard';
import { JiraUserIssuesTable } from '../src/components/JiraUserIssuesTable';
import { Content, Page } from '@backstage/core-components';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import mockEntity from './__fixtures__/entity.json';
import mockJiraResponse from './__fixtures__/jiraResponse.json';
/* Use to test with Jira Cloud
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { Flex, Text } from '@backstage/ui';
import {
Issue,
JiraDataResponse,
JiraResponse,
Project,
} from '@axis-backstage/plugin-jira-dashboard-common';
import multiProjectResponse from './__fixtures__/jiraResponse.json';
import singleProjectResponse from './__fixtures__/singleProjectResponse.json';
import cloudResponse from './__fixtures__/jiraCloudResponse.json';
import multiProjectEntity from './__fixtures__/entity.json';

import mockJiraCloudResponse from './__fixtures__/jiraCloudResponse.json';
*/
// One entity per dashboard variant. The mock API returns a different fixture
// per entity ref, so all three dashboards can coexist in the same dev app.
const singleProjectEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'single-project-service',
description: 'Entity backed by a single Jira Server project',
annotations: { 'jira.com/project-key': 'BS' },
},
spec: { lifecycle: 'production', type: 'service', owner: 'user:guest' },
};

const cloudProjectEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'jenkins-meeting-actions',
description: 'Entity backed by a Jira Cloud project',
annotations: { 'jira.com/project-key': 'MEETING' },
},
spec: { lifecycle: 'production', type: 'service', owner: 'user:guest' },
};

// Fixtures are intentionally partial vs the full Jira schema, hence the casts.
const responsesByEntityRef: Record<string, JiraResponse> = {
[stringifyEntityRef(multiProjectEntity as Entity)]:
multiProjectResponse as unknown as JiraResponse,
[stringifyEntityRef(singleProjectEntity)]:
singleProjectResponse as unknown as JiraResponse,
[stringifyEntityRef(cloudProjectEntity)]:
cloudResponse as unknown as JiraResponse,
};

const userIssues = (singleProjectResponse.data[0].issues ??
[]) as unknown as Issue[];

const mockJiraDashboardApi: JiraDashboardApi = {
getJiraResponseByEntity: async entityRef =>
responsesByEntityRef[entityRef] ??
(multiProjectResponse as unknown as JiraResponse),
getLoggedInUserIssues: async () => userIssues,
getProjectAvatar: async () => undefined,
};

// Standalone fixtures for the component-in-isolation pages.
const sampleProject = singleProjectResponse.project as Project;
const sampleDataGroup = singleProjectResponse
.data[0] as unknown as JiraDataResponse;
const emptyDataGroup: JiraDataResponse = {
name: 'No open issues',
type: 'filter',
query: 'resolution = Unresolved ORDER BY updated DESC',
issues: [],
};
const minimalProject: Project = {
name: 'Minimal Project',
key: 'MIN',
description: '',
avatarUrls: {
'48x48': 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Min',
},
projectTypeKey: '',
lead: { key: '', displayName: '' },
self: 'https://jira.com/project/999',
};

function DevPage(props: { children: ReactNode }) {
return (
<Page themeId="home">
<Content>{props.children}</Content>
</Page>
);
}

function Section(props: { title: string; children: ReactNode }) {
return (
<Flex direction="column" gap="4" mb="6">
<Text variant="title-small">{props.title}</Text>
{props.children}
</Flex>
);
}

createDevApp()
.registerPlugin(jiraDashboardPlugin)
.registerApi({
api: jiraDashboardApiRef,
deps: {},
factory: () =>
({
getJiraResponseByEntity: async () => mockJiraResponse,
getProjectAvatar: async () => mockJiraResponse,
} as any),
factory: () => mockJiraDashboardApi,
})
.addPage({
title: 'Dashboard: Multi-project',
path: '/jira-dashboard',
element: (
<Page themeId="home">
<Content>
<EntityProvider entity={mockEntity}>
<EntityJiraDashboardContent />
</EntityProvider>
</Content>
</Page>
<DevPage>
<EntityProvider entity={multiProjectEntity as Entity}>
<EntityJiraDashboardContent />
</EntityProvider>
</DevPage>
),
})
.addPage({
title: 'Dashboard: Single project',
path: '/jira-dashboard-single',
element: (
<DevPage>
<EntityProvider entity={singleProjectEntity}>
<JiraDashboardContent />
</EntityProvider>
</DevPage>
),
})
.addPage({
title: 'Dashboard: Jira Cloud',
path: '/jira-dashboard-cloud',
element: (
<DevPage>
<EntityProvider entity={cloudProjectEntity}>
<JiraDashboardContent />
</EntityProvider>
</DevPage>
),
})
.addPage({
title: 'JiraTable states',
path: '/jira-table',
element: (
<DevPage>
<Section title="With filters (showFilters)">
<JiraTable
tableContent={sampleDataGroup}
project={sampleProject}
showFilters
/>
</Section>
<Section title="Without filters">
<JiraTable tableContent={sampleDataGroup} project={sampleProject} />
</Section>
<Section title="Empty (no issues)">
<JiraTable
tableContent={emptyDataGroup}
project={sampleProject}
showFilters
/>
</Section>
</DevPage>
),
})
.addPage({
title: 'JiraProjectCard',
path: '/jira-project-card',
element: (
<DevPage>
<Section title="Full project (category, description, lead)">
<JiraProjectCard project={sampleProject} />
</Section>
<Section title="Minimal project (no category/description/lead)">
<JiraProjectCard project={minimalProject} />
</Section>
</DevPage>
),
})
.addPage({
title: 'JiraUserIssuesCard',
path: '/jira-user-issues',
element: (
<DevPage>
<Section title="Card with deep-link footer (bottomLinkProps)">
<JiraUserIssuesCard
title="My open issues"
bottomLinkProps={{
title: 'View all my issues in Jira',
link: 'https://jira.com/issues/?jql=assignee=currentUser()',
}}
/>
</Section>
<Section title="Card without footer">
<JiraUserIssuesCard title="My open issues" />
</Section>
<Section title="Table only (JiraUserIssuesTable)">
<JiraUserIssuesTable />
</Section>
</DevPage>
),
title: 'Root Page',
path: '/jira-dashboard',
})
.render();
3 changes: 1 addition & 2 deletions plugins/jira-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@
"@backstage/frontend-plugin-api": "^0.17.3",
"@backstage/plugin-catalog-react": "^3.2.0",
"@backstage/theme": "^0.7.3",
"@mui/material": "^5.15.7",
"@mui/styles": "^5.15.7",
"@backstage/ui": "^0.17.0",
"lodash": "^4.18.1",
"luxon": "^3.5.0",
"react-use": "^17.2.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import {
ResponseErrorPanel,
TableFilter,
TableOptions,
TabbedCard,
CardTab,
} from '@backstage/core-components';
import { Tab, TabList, TabPanel, Tabs } from '@backstage/ui';
import { useApi } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';
import { EntityInfoCard, useEntity } from '@backstage/plugin-catalog-react';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { jiraDashboardApiRef } from '../../api';
import { useJira } from '../../hooks/useJira';
Expand Down Expand Up @@ -44,18 +43,27 @@ export const JiraDashboardContent = (props?: {
: [];

return projects.length > 1 ? (
<TabbedCard title="Jira Projects" data-testid="tabbed-card">
{projects.map(project => (
<CardTab key={project.key} label={project.name}>
<JiraGrid
project={project}
tableData={jiraResponse.data}
showFilters={props?.showFilters}
tableOptions={props?.tableOptions}
/>
</CardTab>
))}
</TabbedCard>
<EntityInfoCard>
<Tabs>
<TabList aria-label="Jira Projects">
{projects.map(project => (
<Tab id={project.key} key={project.key}>
{project.name}
</Tab>
))}
</TabList>
{projects.map(project => (
<TabPanel id={project.key} key={project.key}>
<JiraGrid
project={project}
tableData={jiraResponse.data}
showFilters={props?.showFilters}
tableOptions={props?.tableOptions}
/>
</TabPanel>
))}
</Tabs>
</EntityInfoCard>
) : (
<JiraGrid
project={projects[0]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Grid from '@mui/material/Grid';
import { Grid } from '@backstage/ui';
import { JiraProjectCard } from '../JiraProjectCard';
import { JiraTable } from '../JiraTable';
import {
Expand All @@ -22,24 +22,18 @@ export const JiraGrid = ({
tableOptions,
}: JiraGridProps) => {
return (
<Grid container spacing={3} style={{ width: '100%' }}>
<Grid
item
xs={12}
xl={6}
<Grid.Root columns="12" gap="6" style={{ width: '100%' }}>
<Grid.Item
colSpan={{ initial: '12', xl: '6' }}
data-testid="project-card"
style={{ paddingTop: '24px', paddingLeft: '24px' }}
>
<JiraProjectCard project={project} />
</Grid>
</Grid.Item>
{tableData.map((value: JiraDataResponse) => (
<Grid
item
xs={12}
xl={6}
<Grid.Item
colSpan={{ initial: '12', xl: '6' }}
key={value.name}
data-testid="issue-table"
style={{ paddingTop: '24px', paddingLeft: '24px' }}
>
<JiraTable
tableContent={value}
Expand All @@ -54,8 +48,8 @@ export const JiraGrid = ({
overflowY: 'auto',
}}
/>
</Grid>
</Grid.Item>
))}
</Grid>
</Grid.Root>
);
};
Loading