feat(usage): plain link to the public usage portal, German opt-in copy
The only way into the usage portal from the settings tab was the session-bound link behind "Connect", which needs an active participation and creates a 15-minute voting session. An operator deciding whether to join had no way to look at the portal first. The status card now carries a plain "Open usage portal" link to the collector base URL the status endpoint already reports, shown whenever that URL is valid, opening in a new tab with rel="noopener noreferrer". The session link stays as it is. German "Teilnahme prüfen" read as a technical check rather than reviewing the consent details; it is now "Details ansehen" in both places the key is used (notice link and opt-in button). The actual opt-in stays "Produktnutzung aktivieren". Relates to issue 1317
This commit is contained in:
@@ -317,3 +317,30 @@ it('states in the consent dialog that the connection only runs outwards', async
|
||||
await screen.findByText('productUsage.sectionOneWay');
|
||||
await screen.findByText('productUsage.oneWay');
|
||||
});
|
||||
|
||||
// The portal is public. Reaching it must not depend on having joined or on
|
||||
// the 15-minute voting session that `connect` creates — an operator deciding
|
||||
// whether to participate should be able to look at it first.
|
||||
describe('the plain usage portal link', () => {
|
||||
it('opens the collector in a new tab before participation, without a session', async () => {
|
||||
mount();
|
||||
const link = await screen.findByRole('link', { name: 'productUsage.openUsagePortal' });
|
||||
expect(link).toHaveAttribute('href', 'https://usage.picpeak.app');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(service.portalSession).not.toHaveBeenCalled();
|
||||
// The session-bound link is a different thing and stays behind `connect`.
|
||||
expect(screen.queryByText('productUsage.openPortal')).toBeNull();
|
||||
});
|
||||
|
||||
it('is not offered when the collector URL is unusable', async () => {
|
||||
vi.mocked(service.status).mockResolvedValue({
|
||||
...status,
|
||||
collector_url: null,
|
||||
collector_error: 'INVALID_COLLECTOR_URL'
|
||||
});
|
||||
mount();
|
||||
await screen.findByText('productUsage.invalidCollectorUrl');
|
||||
expect(screen.queryByRole('link', { name: 'productUsage.openUsagePortal' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../../services/productUsage.service';
|
||||
import {
|
||||
ArrowUpFromLine,
|
||||
ExternalLink,
|
||||
Globe,
|
||||
ListChecks,
|
||||
MessageSquare,
|
||||
@@ -377,14 +378,29 @@ export default function ProductUsageTab() {
|
||||
</>
|
||||
)}
|
||||
{data.collector_url && (
|
||||
<a
|
||||
className="text-sm text-primary-600 dark:text-primary-400 hover:underline self-center"
|
||||
href={`${data.collector_url}/transparency`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('productUsage.transparency')}
|
||||
</a>
|
||||
<>
|
||||
{/* The public portal itself, not the session-bound link below:
|
||||
it needs neither participation nor a voting session, so an
|
||||
operator can look at the portal before deciding to join.
|
||||
Styled as a button so it reads as an action, not a footnote. */}
|
||||
<a
|
||||
className="btn btn-outline btn-md"
|
||||
href={data.collector_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('productUsage.openUsagePortal')}
|
||||
<ExternalLink className="ml-2 h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
className="text-sm text-primary-600 dark:text-primary-400 hover:underline self-center"
|
||||
href={`${data.collector_url}/transparency`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('productUsage.transparency')}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user