Skip to content

Commit

Permalink
Merge pull request #10293 from linode/staging
Browse files Browse the repository at this point in the history
Release v1.115.0 - staging → master
  • Loading branch information
abailly-akamai authored Mar 18, 2024
2 parents d498687 + 496ef42 commit 4b6b353
Show file tree
Hide file tree
Showing 339 changed files with 6,223 additions and 2,587 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
root: true,
ignorePatterns: ["**/node_modules/", "**/build/"],
parser: "@babel/eslint-parser",
parser: "@typescript-eslint/parser",
};
42 changes: 28 additions & 14 deletions docs/development-guide/05-fetching-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,33 +79,47 @@ This works, but has a few disadvantages:

A better way to fetch data is to use React Query. It address the issues listed above and has many additional features.

To fetch data with React Query, check to see if the API method you want to use has a query written for it in `packages/manager/src/queries`. If not, feel free to write one. It should look something like this:
To fetch data with React Query:

```ts
import { getProfile, Profile } from "@linode/api-v4/lib/profile";
import { APIError } from "@linode/api-v4/lib/types";
import { useQuery } from "react-query";
- Create an `@linode/api-v4` function that calls the intended Linode API endpoint.
- Create a query key factory that uses the newly created `@linode/api-v4` function.
- Create a hook that wraps `useQuery` and uses the query key factory.

const queryKey = "profile";
```ts
import { useQuery } from "@tanstack/react-query";
import { getProfile } from "@linode/api-v4";
import type { APIError, Profile } from "@linode/api-v4";

const profileQueries = createQueryKeys('profile', {
profile: {
queryFn: getProfile,
queryKey: null,
},
});

export const useProfile = () =>
useQuery<Profile, APIError[]>(queryKey, getProfile);
useQuery<Profile, APIError[]>(profileQueries.profile);
```

The first time `useProfile()` is called, the data is fetched from the API. On subsequent calls, the data is retrieved from the in-memory cache.

`useQuery` accepts a third "options" parameter, which can be used to specify cache time (among others things). For example, to specify that the cache should never expire for this query:
`useQuery` accepts options which can be used to specify cache time (among others things). For example, to specify that the cache should never expire for this query:

```ts
import { queryPresets } from "src/queries/base";
// ...other imports

const profileQueries = createQueryKeys('profile', {
profile: {
queryFn: getProfile,
queryKey: null,
},
})

export const useProfile = () =>
useQuery<Profile, APIError[]>(
queryKey,
getProfile,
queryPresets.oneTimeFetch
);
useQuery<Profile, APIError[]>({
...profileQueries.profile,
...queryPresets.oneTimeFetch,
});
```

Loading and error states are managed by React Query. The earlier username display example becomes greatly simplified:
Expand Down
16 changes: 16 additions & 0 deletions docs/development-guide/11-feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ Feature flags are served by [LaunchDarkly](https://launchdarkly.com/). On app lo

Feature flag values themselves can be booleans (most common), strings, numbers, or JSON (also common).

We often need to control more than one setting for a given feature. For instance, a feature might have a boolean flag to enable/disable it, and a secondary flag to control its "beta" status. In LaunchDarkly, if using a JSON object and still wanting to control the on/off status of a feature, **all variations must contain an `enabled` key**. ex:

```json
{
"enabled": true,
"beta": true
}
```

Feature flags variations should also be labelled as clearly as possible to avoid confusion for a potential third party managing the flag you created.
For instance, for the example above, the variations could be labelled:

- Variation 1: Feature ON, Beta ON
- Variation 2: Feature ON, Beta OFF
- Variation 3: Everything OFF

## Creating a feature flag

Feature flags are created in the LaunchDarkly dashboard. Give your flag a name (like "Images Pricing Banner") and key (like "imagesPricingBanner") and select the flag type (boolean, etc). Configure the desired variations and targeting options.
Expand Down
25 changes: 22 additions & 3 deletions docs/development-guide/13-coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,28 @@ If you are using VSCode it is highly recommended to use the ESlint extension. Th

## React

- When conditionally rendering JSX, use ternaries instead of `&&`.
- Example: `condition ? <Component /> : null` instead of `condition && <Component />`
- This is to avoid hard-to-catch bugs ([read more](https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx)).
[Several new hooks were introduced with the release of React 18](https://react.dev/blog/2022/03/29/react-v18#new-hooks).

It should be noted that the `useId()` hook is particularly useful for generating unique IDs for accessibility attributes. For this use case, `useId()` is preferred over hardcoding the ID because components may be rendered more than once on a page, but IDs must be unique.

As an example from `DisplayLinodes.tsx`, early in the file we invoke the hook: `const displayViewDescriptionId = React.useId()`

And make use of the unique ID by passing it as the value for a component's `aria-describedby` attribute in the `return` value:

```
<StyledToggleButton
aria-describedby={displayViewDescriptionId}
aria-label="Toggle display"
disableRipple
isActive={true}
onClick={toggleLinodeView}
size="large"
>
<GridView />
</StyledToggleButton>
```

Per the [docs](https://react.dev/reference/react/useId#usage), the hook should not be used for generating keys in a list.

## Event Handler Naming Convention

Expand Down
135 changes: 135 additions & 0 deletions docs/tooling/react-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# React Query

[TanStack Query](https://tanstack.com/query/latest) (formerly React Query) is Cloud Manager's primary tool for fetching and caching API data. For a quick introduction, read our [Fetching Data](../development-guide/05-fetching-data.md#react-query) development guide.

## Query Keys

React Query's cache is a simple key-value store. Query Keys are serializable strings that uniquely identify a query's data in the cache. You can read more about the concept [here](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys) in the TanStack Query docs.

Because of Cloud Manager's complexity, we use [`@lukemorales/query-key-factory`](https://github.com/lukemorales/query-key-factory) to manage our query keys. This package allows us to define query key _factories_ that enable typesafe standardized query keys that can be reused and referenced throughout the application.

### Examples

#### Simple Query

```ts
import { useQuery } from "@tanstack/react-query";
import { getProfile } from "@linode/api-v4";
import type { APIError, Profile } from "@linode/api-v4";

const profileQueries = createQueryKeys('profile', {
profile: {
queryFn: getProfile,
queryKey: null,
},
});

export const useProfile = () =>
useQuery<Profile, APIError[]>(profileQueries.profile);
```

#### Query with parameters

> [!important]
> Queries that have parameters should always include the parameters in the `queryKey`
```ts
import { useQuery } from "@tanstack/react-query";
import { getLinode } from "@linode/api-v4";
import type { APIError, Linode } from "@linode/api-v4";

const linodeQueries = createQueryKeys('linodes', {
linode: (id: number) => ({
queryFn: () => getLinode(id),
queryKey: [id],
}),
});

export const useLinodeQuery = (id: number) =>
useQuery<Linode, APIError[]>(linodeQueries.linode(1));
```

## Maintaining the Cache

> A significant challenge of React Query is keeping the client state in sync with the server.
The two easiest ways of updating the cache using React Query are
- Using `invalidateQueries` to mark data as stale (which will trigger a refetch the next time the query is mounted)
- Using `setQueryData` to manually update the cache

### `invalidateQueries`

This will mark data as stale in the React Query cache, which will cause Cloud Manager to refetch the data if the corresponding query is mounted.

Use `invalidateQueries` when:
- You are dealing with any *paginated data* (because order may have changed)
- You want fresh data from the API

> [!note]
> When using `invalidateQueries`, use a query key factory to ensure you are invalidating the data at the correct query key.
### `setQueryData`

Use this if you have data readily available to put in the cache. This often happens when you make a PUT request.

### Example

This example shows how we keep the cache up to date when performing create / update / delete operations
on an entity.

```ts
import { useQuery, useMutation } from "@tanstack/react-query";
import { getLinode, getLinodes, updateLinode, deleteLinode, createLinode } from "@linode/api-v4";
import type { APIError, Linode, ResourcePage } from "@linode/api-v4";

const linodeQueries = createQueryKeys('linodes', {
linode: (id: number) => ({
queryFn: () => getLinode(id),
queryKey: [id],
}),
linodes: (params: Params = {}, filter: Filter = {}) => ({
queryFn: () => getLinodes(params, filter),
queryKey: [params, filter],
}),
});

export const useLinodeQuery = (id: number) =>
useQuery<Linode, APIError[]>(linodeQueries.linode(1));

export const useLinodeUpdateMutation = (id: number) => {
const queryClient = useQueryClient();
return useMutation<Linode, APIError[], Partial<Linode>>({
mutationFn: (data) => updateLinode(id, data),
onSuccess(linode) {
// Invalidate all paginated pages in the cache.
queryClient.invalidateQueries(linodeQueries.linodes._def);
// Because we have the updated Linode, we can manually set the cache for the `useLinode` query.
queryClient.setQueryData(linodeQueries.linode(id).queryKey, linode);
},
});
}

export const useDeleteLinodeMutation = (id: number) => {
const queryClient = useQueryClient();
return useMutation<{}, APIError[]>({
mutationFn: () => deleteLinode(id),
onSuccess() {
queryClient.removeQueries(linodeQueries.linode(id).queryKey);
queryClient.invalidateQueries(linodeQueries.linodes._def);
},
});
};

export const useCreateLinodeMutation = () => {
const queryClient = useQueryClient();
return useMutation<Linode, APIError[], CreateLinodeRequest>({
mutationFn: createLinode,
onSuccess(linode) {
// Invalidate all paginated pages in the cache. We don't know what page the new Linode will be on.
queryClient.invalidateQueries(linodeQueries.linodes._def);
// Because we have the new Linode, we can manually set the cache for the `useLinode` query.
queryClient.setQueryData(linodeQueries.linode(id).queryKey, linode);
},
});
}
```
13 changes: 13 additions & 0 deletions packages/api-v4/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## [2024-03-18] - v0.112.0


### Changed:

- Make `match_condition` optional in Rule types to support TCP rules ([#10264](https://github.com/linode/manager/pull/10264))
- Make `type` and `region` required in `CreateLinodeRequest` ([#10268](https://github.com/linode/manager/pull/10268))

### Upcoming Features:

- Add Placement Groups events types ([#10221](https://github.com/linode/manager/pull/10221))
- Add temporary deleteBucketWithRegion method for OBJ Multicluster ([#10244](https://github.com/linode/manager/pull/10244))

## [2024-03-04] - v0.111.0

### Changed:
Expand Down
2 changes: 1 addition & 1 deletion packages/api-v4/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@linode/api-v4",
"version": "0.111.0",
"version": "0.112.0",
"homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4",
"bugs": {
"url": "https://github.com/linode/manager/issues"
Expand Down
6 changes: 6 additions & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,12 @@ export type EventAction =
| 'nodebalancer_delete'
| 'nodebalancer_update'
| 'password_reset'
| 'placement_group_assign'
| 'placement_group_created'
| 'placement_group_assigned'
| 'placement_group_unassigned'
| 'placement_group_updated'
| 'placement_group_deleted'
| 'profile_update'
| 'stackscript_create'
| 'stackscript_delete'
Expand Down
6 changes: 3 additions & 3 deletions packages/api-v4/src/aclb/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface Route {
label: string;
protocol: RouteProtocol;
rules: {
match_condition: MatchCondition;
match_condition?: MatchCondition;
service_targets: {
id: number;
label: string;
Expand All @@ -83,7 +83,7 @@ export interface CreateRoutePayload {
}

export interface Rule {
match_condition: MatchCondition;
match_condition?: MatchCondition;
service_targets: {
id: number;
label: string;
Expand All @@ -92,7 +92,7 @@ export interface Rule {
}

export interface RulePayload {
match_condition: MatchCondition;
match_condition?: MatchCondition;
service_targets: {
id: number;
label: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/api-v4/src/linodes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ export interface CreateLinodePlacementGroupPayload {
}

export interface CreateLinodeRequest {
type?: string;
region?: string;
type: string;
region: string;
stackscript_id?: number;
backup_id?: number;
swap_size?: number;
Expand Down
29 changes: 29 additions & 0 deletions packages/api-v4/src/object-storage/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,35 @@ export const deleteBucket = ({
setMethod('DELETE')
);

/**
* deleteBucketWithRegion
*
* Removes a Bucket from your account with region.
*
* NOTE: Attempting to delete a non-empty bucket will result in an error.
*/
/*
@TODO OBJ Multicluster: deleteBucketWithRegion is a function,
once feature is rolled out we replace it with existing deleteBucket
by updating it with region instead of cluster.
*/

export const deleteBucketWithRegion = ({
region,
label,
}: {
region: string;
label: string;
}) =>
Request<ObjectStorageBucket>(
setURL(
`${API_ROOT}/object-storage/buckets/${encodeURIComponent(
region
)}/${encodeURIComponent(label)}`
),
setMethod('DELETE')
);

/**
* Returns a list of Objects in a given Bucket.
*/
Expand Down
Loading

0 comments on commit 4b6b353

Please sign in to comment.