-
Notifications
You must be signed in to change notification settings - Fork 10
Feat/cloud flows #432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/cloud-quickstart-integration
Are you sure you want to change the base?
Feat/cloud flows #432
Changes from all commits
6a222d1
11b4f79
abdcf69
72f5acc
996fef6
1cc0f8d
872c2fc
920d29f
3ed6cfd
a7fed35
ef36acf
a352127
3a7708b
1a92fd7
42df800
f9edadb
a4238a2
676265a
0ce1ebe
6589201
e324ac8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /* | ||
| * Copyright (c) 2026 Nordic Semiconductor ASA | ||
| * | ||
| * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause | ||
| */ | ||
|
|
||
| // TODO: Replace this with a library that can handle ANSI removal. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we can skip adding new dependency, better not to add :) or this regex is not complete? |
||
| const ansiPattern = [ | ||
| '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', | ||
| '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', | ||
| ].join('|'); | ||
|
|
||
| export const cleanShellOutput = (raw: string) => | ||
| raw | ||
| .replace(new RegExp(ansiPattern, 'g'), '') | ||
| .replace(/uart:~\$ ?/g, '') // Zephyr shell prompt | ||
| .replace(/^> ?/gm, ''); // command-echo prompt | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { | |
| getSelectedDeviceUnsafely, | ||
| } from '../../../features/device/deviceSlice'; | ||
| import { Back } from '../../Back'; | ||
| import { cleanShellOutput } from '../../cleanShellOutput'; | ||
| import Main from '../../Main'; | ||
| import { Next, Skip } from '../../Next'; | ||
| import runVerification from './serialport'; | ||
|
|
@@ -58,7 +59,8 @@ export default ({ vComIndex, regex }: { vComIndex: number; regex: RegExp }) => { | |
|
|
||
| useEffect(() => { | ||
| if (!validResponse) { | ||
| const [, match] = (response || '').match(regex) ?? []; | ||
| const stripped = cleanShellOutput(response || ''); | ||
| const [, match] = stripped.match(regex) ?? []; | ||
|
Comment on lines
+62
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this safe for all other devices? |
||
| if (match) { | ||
| setValidResponse(match); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| /* | ||
| * Copyright (c) 2026 Nordic Semiconductor ASA | ||
| * | ||
| * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause | ||
| */ | ||
|
|
||
| import React, { useEffect, useState } from 'react'; | ||
| import { | ||
| Button, | ||
| Dropdown, | ||
| type DropdownItem, | ||
| Spinner, | ||
| } from '@nordicsemiconductor/pc-nrfconnect-shared'; | ||
| import { | ||
| type AuthState, | ||
| inMain as auth, | ||
| } from '@nordicsemiconductor/pc-nrfconnect-shared/ipc/auth'; | ||
|
|
||
| import Logomark from '../../../../../resources/Logomark.png'; | ||
| import { useAppDispatch, useAppSelector } from '../../../../app/store'; | ||
| import { Back } from '../../../../common/Back'; | ||
| import Main from '../../../../common/Main'; | ||
| import { Next, Skip } from '../../../../common/Next'; | ||
| import { connectMemfault, selectOrganization } from './authEffects'; | ||
| import { | ||
| getMemfault, | ||
| nextSubStep, | ||
| prevSubStep, | ||
| resetMemfault, | ||
| setSelectedProjectSlug, | ||
| } from './cloudEvaluateSlice'; | ||
|
|
||
| const emptyItem: DropdownItem<string> = { label: '', value: '' }; | ||
|
|
||
| export default () => { | ||
| const dispatch = useAppDispatch(); | ||
| const memfault = useAppSelector(getMemfault); | ||
| const [authState, setAuthState] = useState<AuthState | null>(null); | ||
| const [authError, setAuthError] = useState<string | null>(null); | ||
|
|
||
| const status = authState?.status; | ||
| const account = authState?.account ?? null; | ||
|
|
||
| const isSignedIn = status === 'signedIn' || status === 'signingOut'; | ||
| const isAuthenticating = status === 'signingIn'; | ||
| const needsReauth = status === 'interactionRequired'; | ||
|
|
||
| useEffect(() => { | ||
| auth.getAuthStatus().then(setAuthState); | ||
| auth.registerOnStateChanged(setAuthState); | ||
| }, []); | ||
|
|
||
| const login = async () => { | ||
| setAuthError(null); | ||
| const res = await auth.startLogin(); | ||
| if (!res.status) { | ||
| setAuthError(res.error); | ||
| return; | ||
| } | ||
| dispatch(resetMemfault()); | ||
| }; | ||
|
|
||
| const logout = async () => { | ||
| await auth.singleSignOut(); | ||
| dispatch(resetMemfault()); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (status === 'signedIn' && memfault.status === 'idle') { | ||
| dispatch(connectMemfault()); | ||
| } | ||
| }, [status, memfault.status, dispatch]); | ||
|
|
||
| const orgItems: DropdownItem<string>[] = memfault.organizations.map(o => ({ | ||
| label: o.name, | ||
| value: o.slug, | ||
| })); | ||
| const projectItems: DropdownItem<string>[] = memfault.projects.map(p => ({ | ||
| label: p.name, | ||
| value: p.slug, | ||
| })); | ||
| const selectedOrgItem = | ||
| orgItems.find(i => i.value === memfault.selectedOrgSlug) ?? emptyItem; | ||
| const selectedProjectItem = | ||
| projectItems.find(i => i.value === memfault.selectedProjectSlug) ?? | ||
| emptyItem; | ||
|
|
||
| const signInPrompt = needsReauth | ||
| ? 'Your session expired. Please sign in again.' | ||
| : 'Sign in to continue.'; | ||
|
|
||
| return ( | ||
| <Main> | ||
| <Main.Content heading="Register your device" subHeading=""> | ||
| <div className="tw-flex tw-flex-col tw-gap-3"> | ||
| <div className="tw-flex tw-flex-col tw-gap-2"> | ||
| <p> | ||
| Connect your device to cloud to capture crashes, | ||
| push OTA updates, and debug remotely. | ||
| </p> | ||
| <ol className="tw-list-inside tw-list-disc"> | ||
| <li>Over-the-air firmware updates</li> | ||
| <li>Remote crash analysis and debugging</li> | ||
| <li> | ||
| Access to DevZone, technical documentation, and | ||
| learning resources | ||
| </li> | ||
| </ol> | ||
| </div> | ||
| {isSignedIn ? ( | ||
| <div className="tw-flex tw-flex-col tw-gap-2 tw-border tw-border-gray-200 tw-p-3"> | ||
| <div className="tw-flex tw-flex-row tw-items-center tw-justify-between"> | ||
| <p> | ||
| Logged in as{' '} | ||
| <b>{account?.name ?? account?.username}</b> | ||
| </p> | ||
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| onClick={logout} | ||
| disabled={status === 'signingOut'} | ||
| > | ||
| {status === 'signingOut' | ||
| ? 'Logging out…' | ||
| : 'Log out'} | ||
| </Button> | ||
| </div> | ||
|
|
||
| {memfault.status === 'loading' && ( | ||
| <div className="tw-flex tw-flex-row tw-items-center tw-gap-2"> | ||
| <Spinner size="sm" /> | ||
| <span> | ||
| Loading organizations and projects… | ||
| </span> | ||
| </div> | ||
| )} | ||
|
|
||
| {memfault.status === 'error' && ( | ||
| <p className="tw-text-red"> | ||
| {memfault.message ?? | ||
| 'Failed to load organizations and projects'} | ||
| </p> | ||
| )} | ||
|
|
||
| {memfault.status === 'success' && ( | ||
| <div className="tw-flex tw-flex-row tw-gap-2"> | ||
| <Dropdown | ||
| label="Organization" | ||
| items={orgItems} | ||
| onSelect={item => | ||
| dispatch( | ||
| selectOrganization(item.value), | ||
| ) | ||
| } | ||
| selectedItem={selectedOrgItem} | ||
| size="sm" | ||
| /> | ||
| <Dropdown | ||
| label="Project" | ||
| items={projectItems} | ||
| onSelect={item => | ||
| dispatch( | ||
| setSelectedProjectSlug( | ||
| item.value, | ||
| ), | ||
| ) | ||
| } | ||
| selectedItem={selectedProjectItem} | ||
| size="sm" | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| <> | ||
| <p className={needsReauth ? 'tw-text-red' : ''}> | ||
| {signInPrompt} | ||
| </p> | ||
| <Button | ||
| variant="secondary" | ||
| size="lg" | ||
| onClick={login} | ||
| className="tw-w-fit" | ||
| disabled={isAuthenticating} | ||
| > | ||
| <div className="tw-flex tw-flex-row tw-items-center tw-justify-center tw-gap-2"> | ||
| <img src={Logomark} alt="myNordic logo" /> | ||
| <span> | ||
| Sign in with myNordic to register your | ||
| device | ||
| </span> | ||
| {isAuthenticating && <Spinner size="sm" />} | ||
| </div> | ||
| </Button> | ||
| {authError && ( | ||
| <p className="tw-text-red">{authError}</p> | ||
| )} | ||
| </> | ||
| )} | ||
| </div> | ||
| </Main.Content> | ||
| <Main.Footer> | ||
| <Back onClick={() => dispatch(prevSubStep())} /> | ||
| {memfault.status === 'error' ? ( | ||
| <> | ||
| <Skip onClick={() => dispatch(nextSubStep())} /> | ||
| <Next | ||
| label="Retry" | ||
| onClick={() => dispatch(connectMemfault())} | ||
| /> | ||
| </> | ||
| ) : ( | ||
| <Next | ||
| disabled={ | ||
| status !== 'signedIn' || | ||
| memfault.status !== 'success' || | ||
| !memfault.selectedProjectSlug | ||
| } | ||
| onClick={() => dispatch(nextSubStep())} | ||
| /> | ||
| )} | ||
| </Main.Footer> | ||
| </Main> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logo looks too small for retina. Is it feasible to use svg or add @2x version? if not quick to do then just ignore