Skip to content

Commit 5460d69

Browse files
committed
Add new End-to-End support table
1 parent 008cc08 commit 5460d69

File tree

6 files changed

+464
-7
lines changed

6 files changed

+464
-7
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import React, { useState, useMemo } from 'react';
2+
import supportData from '../pages/endToEndSupport.json';
3+
import styles from './EndToEndSupportTable.module.css';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
6+
const statusClass = (status) => {
7+
switch (status) {
8+
case 'SUPPORTED':
9+
return styles.statusSupported;
10+
case 'NOT SUPPORTED':
11+
return styles.statusNotSupported;
12+
default:
13+
return '';
14+
}
15+
};
16+
17+
const getOsIconProps = (os) => {
18+
switch (os) {
19+
case 'macOS':
20+
case 'iOS':
21+
return ['fab', 'apple'];
22+
case 'Windows':
23+
return ['fab', 'microsoft'];
24+
case 'Android':
25+
return ['fab', 'android'];
26+
case 'Ubuntu':
27+
return ['fab', 'ubuntu'];
28+
default:
29+
return null;
30+
}
31+
};
32+
33+
const getBrowserIconProps = (browser) => {
34+
switch (browser) {
35+
case 'Chrome':
36+
return ['fab', 'chrome'];
37+
case 'Safari':
38+
return ['fab', 'safari'];
39+
case 'Edge':
40+
return ['fab', 'edge'];
41+
default:
42+
return null;
43+
}
44+
};
45+
46+
const useSortableData = (items, config = null) => {
47+
const [sortConfig, setSortConfig] = useState(config);
48+
49+
const sortedItems = useMemo(() => {
50+
let sortableItems = [...items];
51+
if (sortConfig !== null) {
52+
sortableItems.sort((a, b) => {
53+
if (a[sortConfig.key] < b[sortConfig.key]) {
54+
return sortConfig.direction === 'ascending' ? -1 : 1;
55+
}
56+
if (a[sortConfig.key] > b[sortConfig.key]) {
57+
return sortConfig.direction === 'ascending' ? 1 : -1;
58+
}
59+
return 0;
60+
});
61+
}
62+
return sortableItems;
63+
}, [items, sortConfig]);
64+
65+
const requestSort = (key) => {
66+
let direction = 'ascending';
67+
if (
68+
sortConfig &&
69+
sortConfig.key === key &&
70+
sortConfig.direction === 'ascending'
71+
) {
72+
direction = 'descending';
73+
}
74+
setSortConfig({ key, direction });
75+
};
76+
77+
return { items: sortedItems, requestSort, sortConfig };
78+
};
79+
80+
const columnConfig = [
81+
{ header: 'Client OS', key: 'clientOs' },
82+
{ header: 'Client Browser', key: 'clientBrowser' },
83+
{ header: 'Type & Flow', key: 'typeFlow' },
84+
{ header: 'Protocol', key: 'protocol' },
85+
{ header: 'CM Device', key: 'credentialManagerDevice' },
86+
{ header: 'CM', key: 'credentialManager' },
87+
{ header: 'Status', key: 'status' },
88+
{ header: 'Reason & Solution', key: 'reason' },
89+
];
90+
91+
export default function EndToEndSupportTable() {
92+
const { rows, legend } = supportData;
93+
const { items: sortedRows, requestSort, sortConfig } = useSortableData(rows);
94+
const abbreviations = legend ? legend.abbreviations || {} : {};
95+
const abbreviationItems = Object.entries(abbreviations);
96+
97+
const getSortDirectionClass = (key) => {
98+
if (!sortConfig || sortConfig.key !== key) {
99+
return '';
100+
}
101+
return sortConfig.direction === 'ascending' ? styles.ascending : styles.descending;
102+
};
103+
104+
return (
105+
<>
106+
{abbreviationItems.length > 0 && (
107+
<div className="card margin-bottom--lg">
108+
<div className="card__header">
109+
<h4>Table Legend</h4>
110+
</div>
111+
<div className="card__body">
112+
<div className={styles.legend}>
113+
{abbreviationItems.map(([key, value], index) => (
114+
<React.Fragment key={key}>
115+
<div className={styles.legendItem}>
116+
<strong>{key}:</strong> {value}
117+
</div>
118+
{index < abbreviationItems.length - 1 && (
119+
<div className={styles.divider}></div>
120+
)}
121+
</React.Fragment>
122+
))}
123+
</div>
124+
</div>
125+
</div>
126+
)}
127+
<div style={{ overflowX: 'auto' }}>
128+
<table>
129+
<thead>
130+
<tr>
131+
{columnConfig.map(({ header, key }) => {
132+
const classNames = [styles.sortableHeader, getSortDirectionClass(key)];
133+
if (key !== 'reason') {
134+
classNames.push(styles.cellCentered);
135+
}
136+
return (
137+
<th
138+
key={key}
139+
onClick={() => requestSort(key)}
140+
className={classNames.join(' ')}
141+
>
142+
{header}
143+
</th>
144+
);
145+
})}
146+
</tr>
147+
</thead>
148+
<tbody>
149+
{sortedRows.map((row, index) => (
150+
<tr key={index}>
151+
{columnConfig.map(({ key }) => {
152+
const classNames = [
153+
key !== 'reason' ? styles.cellCentered : '',
154+
['clientOs', 'clientBrowser', 'credentialManagerDevice'].includes(key) ? styles.noWrapCell : '',
155+
]
156+
.filter(Boolean)
157+
.join(' ');
158+
159+
let content;
160+
const cellValue = row[key];
161+
let iconProps;
162+
163+
if (key === 'clientOs' || key === 'credentialManagerDevice') {
164+
iconProps = getOsIconProps(cellValue);
165+
} else if (key === 'clientBrowser') {
166+
iconProps = getBrowserIconProps(cellValue);
167+
}
168+
169+
if (iconProps) {
170+
content = <>
171+
<FontAwesomeIcon icon={iconProps} style={{ marginRight: '8px' }} />
172+
{cellValue}
173+
</>;
174+
} else if (key === 'status') {
175+
content =
176+
<span className={`${styles.statusBadge} ${statusClass(row[key])}`}>
177+
{row[key]}
178+
</span>;
179+
} else {
180+
content = cellValue;
181+
}
182+
183+
return (
184+
<td key={key} className={classNames}>
185+
{content}
186+
</td>
187+
);
188+
})}
189+
</tr>
190+
))}
191+
</tbody>
192+
</table>
193+
</div>
194+
</>
195+
);
196+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.statusBadge {
2+
border-radius: 0.75rem; /* 12px */
3+
color: #ffffff;
4+
display: inline-block;
5+
font-size: 0.8em;
6+
font-weight: 600;
7+
line-height: 1;
8+
padding: 0.25rem 0.5rem; /* 4px 8px */
9+
text-align: center;
10+
text-transform: uppercase;
11+
}
12+
13+
.statusSupported {
14+
background-color: green;
15+
}
16+
17+
.statusNotSupported {
18+
background-color: red;
19+
}
20+
21+
.cellCentered {
22+
text-align: center;
23+
}
24+
25+
.sortableHeader {
26+
cursor: pointer;
27+
user-select: none;
28+
}
29+
30+
.ascending::after {
31+
content: ' ▲';
32+
font-size: 0.8em;
33+
}
34+
35+
.descending::after {
36+
content: ' ▼';
37+
font-size: 0.8em;
38+
}
39+
40+
.legend {
41+
margin-top: 1rem;
42+
font-size: 0.9em;
43+
display: flex;
44+
flex-wrap: wrap;
45+
align-items: center;
46+
gap: 0.5rem;
47+
}
48+
49+
.legendItem {
50+
}
51+
52+
.divider {
53+
}
54+
55+
.noWrapCell {
56+
white-space: nowrap;
57+
}
58+
59+
@media (max-width: 996px) {
60+
.noWrapCell {
61+
white-space: normal;
62+
}
63+
}

src/pages/ecosystem-support.mdx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ hide_table_of_contents: true
66
import Tabs from "@theme/Tabs";
77
import TabItem from "@theme/TabItem";
88
import DigitalCredentialsApiSupportTable from "@site/src/components/DigitalCredentialsApiSupportTable";
9+
import EndToEndSupportTable from "@site/src/components/EndToEndSupportTable";
910
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1011

1112
# Ecosystem Support
@@ -15,15 +16,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1516
The Digital Credentials Web Platform API
1617

1718
<DigitalCredentialsApiSupportTable />
18-
</TabItem>
19-
<TabItem value="app-pres-req" label="Digital Credentials API (Native)">
20-
21-
> Coming Soon
2219

2320
</TabItem>
24-
<TabItem value="app-wallet" label="Credential Manager APIs">
21+
<TabItem value="end2end" label="End-to-End">
22+
The table below conveys end-to-end combinations of operating systems, browsers, protocols, and credential managers.
2523

26-
> Coming Soon
24+
A couple of notes about the table:
2725

28-
</TabItem>
26+
- Browsers which do not support the Digital Credentials API are not listed (see the [Digital Credentials API (Web) tab](ecosystem-support?support-matrix=dc-api)).
27+
- Client OS refers to the device where the Digital Credentials API is called.
28+
<EndToEndSupportTable />
29+
</TabItem>
2930
</Tabs>

src/pages/endToEndSupport.js

Whitespace-only changes.

0 commit comments

Comments
 (0)