Skip to content

Commit

Permalink
Merge branch 'develop' into unit-test-stability
Browse files Browse the repository at this point in the history
  • Loading branch information
Joe D'Amore committed Jun 5, 2024
2 parents 0cdb44e + d52ad40 commit a40637f
Show file tree
Hide file tree
Showing 1,343 changed files with 50,609 additions and 22,739 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h3 align="center">
<img src="https://github.com/linode/manager/blob/develop/packages/manager/src/assets/logo/akamai-logo.svg" width="200" />
<img src="https://github.com/linode/manager/blob/develop/packages/manager/src/assets/logo/akamai-logo-color.svg" width="200" />
<br />
<br />
Akamai Connected Cloud Manager
Expand Down
98 changes: 98 additions & 0 deletions docs/development-guide/05-fetching-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,104 @@ console.log(errorMap);
}
```

#### Scrolling to errors

For deep forms, we provide a utility that will scroll to the first error encountered within a defined container. We do this to improve error visibility, because the user can be unaware of an error that isn't in the viewport.
An error can be a notice (API error) or a Formik field error. In order to implement this often needed functionality, we must declare a form or form container via ref, then pass it to the `scrollErrorIntoViewV2` util (works both for class & functional components).

Note: the legacy `scrollErrorIntoView` is deprecated in favor of `scrollErrorIntoViewV2`.

Since Cloud Manager uses different ways of handling forms and validation, the `scrollErrorIntoViewV2` util should be implemented using the following patterns to ensure consistency.

##### Formik
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const {
values,
// other handlers
} = useFormik({
initialValues: {},
onSubmit: mySubmitFormHandler,
validate: () => {
scrollErrorIntoViewV2(formRef);
},
validationSchema: myValidationSchema,
});

return (
<form onSubmit={handleSubmit} ref={formContainerRef}>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
);
};
```

##### React Hook Forms
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const methods = useForm<LinodeCreateFormValues>({
defaultValues,
mode: 'onBlur',
resolver: myResolvers,
// other methods
});

return (
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(onSubmit, () => scrollErrorIntoViewV2(formRef))}
ref={formContainerRef}
>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
</>
);
};
```

##### Uncontrolled forms
```Typescript
import * as React from 'react';

import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

export const MyComponent = () => {
const formContainerRef = React.useRef<HTMLFormElement>(null);

const handleSubmit = () => {
try {
// form submission logic
} catch {
scrollErrorIntoViewV2(formContainerRef);
}
};

return (
<form onSubmit={handleSubmit} ref={formContainerRef}>
<Error />
{/* form fields */}
<button type="submit">Submit</button>
</form>
);
};
```

### Toast / Event Message Punctuation
**Best practice:**
- If a message is a sentence or a sentence fragment with a subject and a verb, add punctuation. Otherwise, leave punctuation off.
Expand Down
4 changes: 2 additions & 2 deletions docs/development-guide/09-mocking-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ To enable the MSW, open the Local Dev Tools and check the "Mock Service Worker"
import { rest } from "msw";

const handlers = [
rest.get("*/profile", (req, res, ctx) => {
http.get("*/profile", () => {
//
const profile = profileFactory.build({ restricted: true });
return res(ctx.json(profile));
return HttpResponse.json((profile));
}),
// ... other handlers
];
Expand Down
45 changes: 18 additions & 27 deletions docs/development-guide/13-coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ The styles for Cloud Manager are located in three places:
- The breakpoints can be modified at `/foundations/breakpoints/index.ts`.
- Component-specific styles may be defined either at the end of the component file or in a dedicated file, named `ComponentName.styles.tsx`. Refer to the guidelines outlined in the "Styles" section of [Component Structure](02-component-structure.md#styles).

## Typesript Unions, Const Enums and Objects
## Typescript Unions, Const Enums, Objects and Intersections
In our development process, we often encounter scenarios where we need to handle various messages or descriptions in our application. These messages can range from short, pithy statements to longer, more descriptive texts. To ensure a consistent and maintainable approach, we can use union types for lists of pithy data and const enums or plain old JavaScript objects (POJOs) for longer descriptions.

### Union Types for Pithy Data
Expand Down Expand Up @@ -132,38 +132,29 @@ myFunction(CreateTypes.Backup); // Works
myFunction('fromBackup'); // Works
```

## Adobe Analytics
### Preferring Interfaces Over Intersections

### Writing a Custom Event
Much of the time, a simple type alias to an object type acts very similarly to an interface.

Custom events live (mostly) in `src/utilities/analytics.ts`. Try to write and export custom events in this file if possible, and import them in the component(s) where they are used.
```Typescript
interface Foo { prop: string }

```tsx
// Component.tsx {file(s) where the event is called, for quick reference}
// OtherComponent.tsx

sendDescriptiveNameEvent () => {
category: '{Descriptive/Page Name}',
action: '{interaction such as Click, Hover, Focus}:{input type such as button, link, toggle, checkbox, text field} e.g. Click:link',
label: '{string associated with the event; e.g event label} (optional)',
value: '{number associated with the event; e.g. size of a volume} (optional)',
}
type Bar = { prop: string };
```

When adding a new custom event, coordinating with UX on the event's `category`, `action`, `label`, and `value` props values ensures consistency across our data.

Avoid including pipes (`|`) as delimiters in any of the event properties. They are used in Adobe Analytics to separate fields.
However, and as soon as you need to compose two or more types, you have the option of extending those types with an interface, or intersecting them in a type alias, and that's when the differences start to matter.

Avoid creating custom events that collect such as search queries, entity names, or other forms of user input. Custom events can still be fired on these actions with a generic `label` or no label at all.
Interfaces create a single flat object type that detects property conflicts, which are usually important to resolve! Intersections on the other hand just recursively merge properties, and in some cases produce never. Interfaces also display consistently better, whereas type aliases to intersections can't be displayed in part of other intersections. Type relationships between interfaces are also cached, as opposed to intersection types as a whole. A final noteworthy difference is that when checking against a target intersection type, every constituent is checked before checking against the "effective"/"flattened" type.

Examples
For this reason, extending types with interfaces/extends is suggested over creating intersection types.

- `sendMarketplaceSearchEvent` fires when selecting a category from the dropdown (`label` is predefined) and clicking the search field (a generic `label` is used).
- `sendBucketCreateEvent` sends the region of the bucket, but does not send the bucket label.

### Locally Testing Page Views & Custom Events and/or Troubleshooting
```Typescript
- type Foo = Bar & Baz & {
- someProp: string;
- }
+ interface Foo extends Bar, Baz {
+ someProp: string;
+ }
```
1. Set the `REACT_APP_ADOBE_ANALYTICS_URL` environment variable in `.env`.
2. Use the browser tools Network tab, filter requests by "adobe", and check that successful network requests have been made to load the launch script and its extensions.
3. In the browser console, type `_satellite.setDebug(true)`.
4. Refresh the page. You should see Adobe debug log output in the console. Each page view change or custom event that fires should be visible in the logs.
Source: [TypeScript Wiki](https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections)
73 changes: 73 additions & 0 deletions docs/development-guide/15-api-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# API Events

In order to display Events, Cloud Manager polls the [account/events](https://www.linode.com/docs/api/account/#events-list) endpoint at a 16 second interval, or every 2 seconds if there are “in-progress” events.

In order to display these messages in the application (Notification Center, /events page), we compose messages according to the Event key (`EventAction`). Each key requires an entry and set of custom messages for each status (`EventStatus`), dictated by API specs. Not every Status is required for a given Action.

## Adding a new Action and Composing Messages

In order to add a new Action, one must add a new key to the read-only `EventActionKeys` constant array in the api-v4 package.
Once that's done, a related entry must be added to the `eventMessages` Event Map. In order to do so, the entry can either be added to an existing Event Factory or a new one. `eventMessages` is strictly typed, so the decision where to add the new Action will be clear. ex:

```Typescript
import { EventLink } from '../EventLink';

import type { PartialEventMap } from '../types';

export const linode: PartialEventMap<'linode'> = {
linode_addip: {
notification: (e) => (
<>
An IP address has been <strong>added</strong> to Linode{' '}
<EventLink event={e} to="entity" />.
</>
),
},
};
```

The convention to compose the message is as follows:
- Use the `<EventLink />` component for linking `entity` or `secondary_entity`. This component includes a lookup util to format the link `href` according to the feature.
- The bolding should only be applied to:
- the primary action: (ex: `<strong>created</strong>`)
- its correlated negation for negative actions (ex: `could <strong>not</strong> be <strong>created</strong>.`)
- The `message` should be also handled via the `<EventMessage message={e.message} />` in order to handle potential formatting from the API string (ticks to indicate code blocks).

## Displaying Events in snackbars

We can leverage the Event Message factories in order to display events in snackbars/toasts when a given action gets triggered via APIv4.

```Typescript
const { enqueueSnackbar } = useSnackbar();

try {
const successMessage = getEventMessage({
action: 'image_upload',
entity: {
label: 'Entity',
url: '/image/123',
},
status: 'notification',
});

const showToast = (variant: any) =>
enqueueSnackbar(successMessage, {
'success',
});
}, catch {
const failureMessage = getEventMessage({
action: 'image_upload',
// in this case we don't add an entity since we know we can't link to it
status: 'failed',
});

const showToast = (variant: any) =>
enqueueSnackbar(failureMessage, {
'error',
});
}
```

Both `action` and `status` are required. The `entity` and `secondary_entity` can optionally be passed to allow for linking. **Note**: it is possible the Event Message linking will be missing if the action status message expects either value but isn't not provided by the instance call.

If a corresponding status does not exist (ex: "failed"), it's encouraged to add it to the Action. Event if not triggered by the API, it can be useful to have a reusable Event Message to use through the App.
42 changes: 42 additions & 0 deletions docs/tooling/analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Adobe Analytics

Cloud Manager uses Adobe Analytics to capture page view and custom event statistics. To view analytics, Cloud Manager developers must follow internal processes to request access to Adobe Analytics dashboards.

## Writing a Custom Event

Custom events live (mostly) in `src/utilities/analytics/customEventAnalytics.ts`. Try to write and export custom events in this file if possible, and import them in the component(s) where they are used.

A custom event will take this shape:
```tsx
// Component.tsx {file(s) where the event is called, for quick reference}
// OtherComponent.tsx

sendDescriptiveNameEvent () => {
category: '{Descriptive/Page/Flow Name}',
action: '{interaction such as Click, Hover, Focus}:{input type such as button, link, toggle, checkbox, text field} e.g. Click:link',
label: '{string associated with the event; e.g event label} (optional)',
value: '{number associated with the event; e.g. size of a volume} (optional)',
data: '{stringified object of additional key-value pairs; e.g. "{isLinodePoweredOff: true}"} (optional)'
}
```

When adding a new custom event, coordinate with UX on the event's `category`, `action`, `label`, and `value` variables to ensure consistency across our data.

`data` is an additional variable we use to capture information associated with an event that cannot easily or clearly be represented via the other variables; for example, boolean key-value pair(s). To add an additional property to `data`, it should first be added as an optional property and typed in the `CustomAnalyticsData` interface.

Avoid including pipes (`|`) as delimiters in any of the event properties. They are used in Adobe Analytics to separate fields.

Avoid creating custom events that collect such as search queries, entity names, or other forms of user input. Custom events can still be fired on these actions with a generic `label` or no label at all.

Examples

- `sendMarketplaceSearchEvent` fires when selecting a category from the dropdown (`label` is predefined) and clicking the search field (a generic `label` is used).
- `sendBucketCreateEvent` sends the region of the bucket, but does not send the bucket label.

## Locally Testing Page Views & Custom Events and/or Troubleshooting

1. Set the `REACT_APP_ADOBE_ANALYTICS_URL` environment variable in `.env`.
2. Use the browser tools Network tab, filter requests by "adobe", and check that successful network requests have been made to load the launch script and its extensions.
3. In the browser console, type `_satellite.setDebug(true)`.
4. Refresh the page. You should see Adobe debug log output in the console. Each page view change or custom event that fires should be visible in the logs.
5. When viewing dashboards in Adobe Analytics, it may take ~1 hour for analytics data to update. Once this happens, locally fired events will be visible in the dev dashboard.
Loading

0 comments on commit a40637f

Please sign in to comment.