Fixing Singleton typing to correctly return child class (#1914)

- Add distinction between `getInstance` and `getInstanceOrCreate` since
  it is not always possible to create an instance (since you might not
  know the correct arguments)

- Remove all the `export const *Store = *Store.getInstance<*Store>();`
  calls as it defeats the purpose of `Singleton`. Plus with the typing
  changes the appropriate `*Store.getInstance()` is "short enough".

- Special case the two extension export facades to not need to use
  `getInstanceOrCreate`. Plus since they are just facades it is always
  possible to create them.

- Move some other types to be also `Singleton`'s: ExtensionLoader,
  ExtensionDiscovery, ThemeStore, LocalizationStore, ...

- Fixed dev-run always using the same port with electron inspect

- Update Store documentation with new recommendations about creating
  instances of singletons

- Fix all unit tests to create their dependent singletons

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-04-21 09:59:59 -04:00
1 parent b2a570ce28
commit 9563ead2e6
114 files changed
+924 -736

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ Each guide or code sample includes the following:
| Sample | APIs |
| ----- | ----- |
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.ClusterStore <br> Store.workspaceStore |
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
+2 -4
View File
@@ -45,8 +45,6 @@ It accesses some Lens state data, and it periodically logs the name of the clust
```typescript
import { LensMainExtension, Store } from "@k8slens/extensions";
const clusterStore = Store.clusterStore
export default class ActiveClusterExtensionMain extends LensMainExtension {
timer: NodeJS.Timeout
@@ -54,11 +52,11 @@ export default class ActiveClusterExtensionMain extends LensMainExtension {
onActivate() {
console.log("Cluster logger activated");
this.timer = setInterval(() => {
if (!clusterStore.active) {
if (!Store.ClusterStore.getInstance().active) {
console.log("No active cluster");
return;
}
console.log("active cluster is", clusterStore.active.contextName)
console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName)
}, 5000)
}
+1 -1
View File
@@ -57,7 +57,7 @@ The example above logs messages when the extension is enabled and disabled.
Cluster pages appear in the cluster dashboard.
Use cluster pages to display information about or add functionality to the active cluster.
It is also possible to include custom details from other clusters.
Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore).
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
+28 -25
View File
@@ -15,6 +15,12 @@ This guide shows how to create a store for the [`appPreferences`](../renderer-ex
The preference is a simple boolean that indicates whether or not something is enabled.
However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted.
`Store.ExtensionStore`'s child class will need to be created before being used.
It is recommended to call the inherited static method `getInstanceOrCreate()` only in one place, generally within you extension's `onActivate()` method.
It is also recommenced to delete the instance, using the inherited static method `resetInstance()`, in your extension's `onDeactivate()` method.
Everywhere else in your code you should use the `getInstance()` static method.
This is so that your data is kept up to date and not persisted longer than expected.
The following example code creates a store for the `appPreferences` guide example:
``` typescript
@@ -50,8 +56,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
});
}
}
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
```
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type.
@@ -71,46 +75,51 @@ It is called when the store is being saved.
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension.
Note that `examplePreferencesStore` is a singleton.
Calling this function again will not create a new store.
Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
Note that `ExamplePreferencesStore` is a singleton.
Calling this function will create an instance if one has not been made before.
Through normal use you should call `ExamplePreferencesStore.getInstance()` as that will throw an error if an instance does not exist.
This provides some logical safety in that it limits where a new instance can be created.
Thus it prevents an instance from being created when the constructor args are not present at the call site.
If you are doing some cleanup it is recommended to call `ExamplePreferencesStore.getInstance(false)` which returns `undefined` instead of throwing when there is no instance.
The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store.
`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
`ExamplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
This can be done in `./main.ts`:
``` typescript
import { LensMainExtension } from "@k8slens/extensions";
import { examplePreferencesStore } from "./src/example-preference-store";
import { ExamplePreferencesStore } from "./src/example-preference-store";
export default class ExampleMainExtension extends LensMainExtension {
async onActivate() {
await examplePreferencesStore.loadExtension(this);
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
}
}
```
Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
Here, `ExamplePreferencesStore` loads with `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
This can be done in `./renderer.ts`:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
import { examplePreferencesStore } from "./src/example-preference-store";
import { ExamplePreferencesStore } from "./src/example-preference-store";
import React from "react";
export default class ExampleRendererExtension extends LensRendererExtension {
async onActivate() {
await examplePreferencesStore.loadExtension(this);
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
}
appPreferences = [
{
title: "Example Preferences",
components: {
Input: () => <ExamplePreferenceInput preference={examplePreferencesStore}/>,
Input: () => <ExamplePreferenceInput />,
Hint: () => <ExamplePreferenceHint/>
}
}
@@ -118,8 +127,8 @@ export default class ExampleRendererExtension extends LensRendererExtension {
}
```
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is called to load `ExamplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
``` typescript
@@ -128,21 +137,15 @@ import { observer } from "mobx-react";
import React from "react";
import { ExamplePreferencesStore } from "./example-preference-store";
export class ExamplePreferenceProps {
preference: ExamplePreferencesStore;
}
@observer
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
export class ExamplePreferenceInput extends React.Component {
render() {
const { preference } = this.props;
return (
<Component.Checkbox
label="I understand appPreferences"
value={preference.enabled}
onChange={v => { preference.enabled = v; }}
value={ExamplePreferencesStore.getInstace().enabled}
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
/>
);
}
@@ -159,4 +162,4 @@ export class ExamplePreferenceHint extends React.Component {
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
`examplePreferencesStore`.
`ExamplePreferencesStore`.