@openshift/dynamic-plugin-sdk-utils
Advanced tools
Comparing version 1.0.0-alpha3 to 1.0.0-alpha4
@@ -0,1 +1,5 @@ | ||
import { PluginStore } from '@openshift/dynamic-plugin-sdk'; | ||
import * as React from 'react'; | ||
import { Store, AnyAction } from 'redux'; | ||
import { ActionType } from 'typesafe-actions'; | ||
import { K8sVerb } from '@openshift/dynamic-plugin-sdk/src/types/core'; | ||
@@ -174,12 +178,2 @@ | ||
declare const commonFetch: (url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined) => Promise<Response>; | ||
declare const commonFetchText: (url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined) => Promise<string>; | ||
declare const commonFetchJSON: { | ||
<TResult>(url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult>; | ||
put<TResult_1>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_1>; | ||
post<TResult_2>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_2>; | ||
patch<TResult_3>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_3>; | ||
delete<TResult_4>(url: string, data?: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_4>; | ||
}; | ||
declare type K8sResourceIdentifier = { | ||
@@ -299,2 +293,43 @@ apiGroup?: string; | ||
declare type InitAPIDiscovery = (store: Store<unknown, ActionType<AnyAction>>) => void; | ||
declare type AppInitSDKProps = { | ||
/** Only child is your Application */ | ||
children: React.ReactElement; | ||
configurations: { | ||
apiDiscovery?: InitAPIDiscovery; | ||
appFetch: UtilsConfig['appFetch']; | ||
pluginStore: PluginStore; | ||
wsAppSettings: UtilsConfig['wsAppSettings']; | ||
}; | ||
}; | ||
/** | ||
* Initializes the host application to work with Kubernetes and related SDK utilities. | ||
* Add this at app-level to make use of app's redux store and pass configurations prop needed to initialize the app, preferred to have it under Provider. | ||
* It checks for store instance if present or not. | ||
* If the store is there then the reference is persisted to be used in SDK else it creates a new store and passes it to the children with the provider | ||
* @component AppInitSDK | ||
* @example | ||
* ```tsx | ||
* return ( | ||
* <Provider store={store}> | ||
* <AppInitSDK configurations={{ appFetch, pluginStore, wsAppSettings }}> | ||
* <App /> | ||
* </AppInitSDK> | ||
* </Provider> | ||
* ) | ||
* ``` | ||
*/ | ||
declare const AppInitSDK: React.FC<AppInitSDKProps>; | ||
declare const commonFetch: (url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined) => Promise<Response>; | ||
declare const commonFetchText: (url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined) => Promise<string>; | ||
declare const commonFetchJSON: { | ||
<TResult>(url: string, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult>; | ||
put<TResult_1>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_1>; | ||
post<TResult_2>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_2>; | ||
patch<TResult_3>(url: string, data: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_3>; | ||
delete<TResult_4>(url: string, data?: unknown, requestInit?: RequestInit | undefined, timeout?: number | undefined, isK8sAPIRequest?: boolean | undefined): Promise<TResult_4>; | ||
}; | ||
declare type WatchK8sResult<R extends K8sResourceCommon | K8sResourceCommon[]> = [ | ||
@@ -480,2 +515,2 @@ data: R, | ||
export { BulkMessageHandler, CloseHandler, DestroyHandler, ErrorHandler, K8sModelCommon, K8sResourceCommon, MessageHandler, OpenHandler, UtilsConfig, WebSocketFactory, WebSocketOptions, WebSocketState, commonFetch, commonFetchJSON, commonFetchText, getK8sResourceURL, getUtilsConfig, isUtilsConfigSet, k8sCreateResource, k8sDeleteResource, k8sGetResource, k8sListResource, k8sListResourceItems, k8sPatchResource, k8sUpdateResource, setUtilsConfig, useK8sWatchResource }; | ||
export { AppInitSDK, BulkMessageHandler, CloseHandler, DestroyHandler, ErrorHandler, K8sModelCommon, K8sResourceCommon, MessageHandler, OpenHandler, UtilsConfig, WebSocketFactory, WebSocketOptions, WebSocketState, commonFetch, commonFetchJSON, commonFetchText, getK8sResourceURL, getUtilsConfig, isUtilsConfigSet, k8sCreateResource, k8sDeleteResource, k8sGetResource, k8sListResource, k8sListResourceItems, k8sPatchResource, k8sUpdateResource, setUtilsConfig, useK8sWatchResource }; |
@@ -5,44 +5,18 @@ /* | ||
@openshift/dynamic-plugin-sdk-utils version 1.0.0-alpha3 | ||
March 8, 2022 at 10:18:12 PM GMT+1 | ||
commit b1138726d539870ac4eaef5a1d8eb1717473bb53 | ||
@openshift/dynamic-plugin-sdk-utils version 1.0.0-alpha4 | ||
March 9, 2022 at 3:49:58 PM GMT+1 | ||
commit d81643bdebaa9b288c8758554a733891a14fdaf7 | ||
*/ | ||
import * as _ from 'lodash-es'; | ||
import { PluginStoreProvider } from '@openshift/dynamic-plugin-sdk'; | ||
import * as React from 'react'; | ||
import { useSelector, useDispatch } from 'react-redux'; | ||
import { useStore, Provider, useSelector, useDispatch } from 'react-redux'; | ||
import { plural } from 'pluralize'; | ||
import { Map, fromJS } from 'immutable'; | ||
import { action } from 'typesafe-actions'; | ||
import 'immutable'; | ||
import { createStore, combineReducers, compose, applyMiddleware } from 'redux'; | ||
import thunk from 'redux-thunk'; | ||
let config; | ||
/** | ||
* Checks if the {@link UtilsConfig} is set. | ||
*/ | ||
const isUtilsConfigSet = () => { | ||
return config !== undefined; | ||
}; | ||
/** | ||
* Set the {@link UtilsConfig} reference. | ||
* | ||
* This must be done before using any of the Kubernetes utilities. | ||
*/ | ||
const setUtilsConfig = (c) => { | ||
if (config !== undefined) { | ||
throw new Error('UtilsConfig reference has already been set'); | ||
} | ||
config = Object.freeze({ ...c }); | ||
}; | ||
/** | ||
* Get the {@link UtilsConfig} reference. | ||
* | ||
* Throws an error if the reference isn't already set. | ||
*/ | ||
const getUtilsConfig = () => { | ||
if (config === undefined) { | ||
throw new Error('UtilsConfig reference has not been set'); | ||
} | ||
return config; | ||
}; | ||
/** | ||
* Base class for custom errors. | ||
@@ -96,2 +70,52 @@ * | ||
let config; | ||
/** | ||
* Checks if the {@link UtilsConfig} is set. | ||
*/ | ||
const isUtilsConfigSet = () => { | ||
return config !== undefined; | ||
}; | ||
/** | ||
* Set the {@link UtilsConfig} reference. | ||
* | ||
* This must be done before using any of the Kubernetes utilities. | ||
*/ | ||
const setUtilsConfig = (c) => { | ||
if (config !== undefined) { | ||
throw new Error('UtilsConfig reference has already been set'); | ||
} | ||
config = Object.freeze({ ...c }); | ||
}; | ||
/** | ||
* Get the {@link UtilsConfig} reference. | ||
* | ||
* Throws an error if the reference isn't already set. | ||
*/ | ||
const getUtilsConfig = () => { | ||
if (config === undefined) { | ||
throw new Error('UtilsConfig reference has not been set'); | ||
} | ||
return config; | ||
}; | ||
let reduxStore; | ||
/** | ||
* Set the {@link Store} reference. | ||
* | ||
* This must be done before using the AppInitSDK React entrypoint | ||
*/ | ||
const setReduxStore = (storeData) => { | ||
reduxStore = storeData; | ||
}; | ||
/** | ||
* Get the {@link Store} reference. | ||
* | ||
* Throws an error if the reference isn't already set. | ||
*/ | ||
const getReduxStore = () => { | ||
if (reduxStore === undefined) { | ||
throw new Error('Redux store reference has not been set'); | ||
} | ||
return reduxStore; | ||
}; | ||
class TimeoutError extends CustomError { | ||
@@ -599,2 +623,17 @@ constructor(url, ms) { | ||
const getReferenceForModel = (model) => getReference({ group: model.apiGroup, version: model.apiVersion, kind: model.kind }); | ||
// TODO Migrate implementation or refactor K8s reducer to avoid usage | ||
let k8sModels; | ||
const allModels = () => { | ||
if (!k8sModels) { | ||
k8sModels = Map(); | ||
} | ||
return k8sModels; | ||
}; | ||
let namespacedResources; | ||
const getNamespacedResources = () => { | ||
if (!namespacedResources) { | ||
namespacedResources = new Set(); | ||
} | ||
return namespacedResources; | ||
}; | ||
/** | ||
@@ -656,2 +695,13 @@ * @deprecated - This will become obsolete when we move away from K8sResourceKindReference to K8sGroupVersionKind | ||
const k8sListResourceItems = (options) => k8sListResource(options).then((result) => result.items); | ||
const abbrBlacklist = ['ASS']; | ||
/** | ||
* Provides an abbreviation string for given kind with respect to abbrBlacklist. | ||
* @param kind Kind for which the abbreviation is generated. | ||
* @return Abbreviation string for given kind. | ||
* TODO: Use in resource-icon component once it is being migrated to the SDK. | ||
* * */ | ||
const kindToAbbr = (kind) => { | ||
const abbrKind = (kind.replace(/[^A-Z]/g, '') || kind.toUpperCase()).slice(0, 4); | ||
return abbrBlacklist.includes(abbrKind) ? abbrKind.slice(0, -1) : abbrKind; | ||
}; | ||
@@ -701,3 +751,3 @@ /** | ||
const WS = {}; | ||
const POLLs = {}; | ||
const POLLs$1 = {}; | ||
const REF_COUNTS = {}; | ||
@@ -715,5 +765,5 @@ const paginationLimit = 250; | ||
} | ||
const poller = POLLs[id]; | ||
const poller = POLLs$1[id]; | ||
window.clearInterval(poller); | ||
delete POLLs[id]; | ||
delete POLLs$1[id]; | ||
delete REF_COUNTS[id]; | ||
@@ -780,3 +830,3 @@ dispatch(stopWatchK8s(id)); | ||
const pollAndWatch = async () => { | ||
delete POLLs[id]; | ||
delete POLLs$1[id]; | ||
try { | ||
@@ -797,4 +847,4 @@ const resourceVersion = await incrementallyLoad(); | ||
consoleLogger.warn('Resource does not support watching, falling back to polling.', k8skind); | ||
if (!POLLs[id]) { | ||
POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
if (!POLLs$1[id]) { | ||
POLLs$1[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
} | ||
@@ -814,4 +864,4 @@ return; | ||
dispatch(errored(id, e)); | ||
if (!POLLs[id]) { | ||
POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
if (!POLLs$1[id]) { | ||
POLLs$1[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
} | ||
@@ -838,6 +888,6 @@ return; | ||
delete WS[id]; | ||
if (POLLs[id]) { | ||
if (POLLs$1[id]) { | ||
return; | ||
} | ||
POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
POLLs$1[id] = window.setTimeout(pollAndWatch, 15 * 1000); | ||
}) | ||
@@ -886,3 +936,3 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
}; | ||
POLLs[id] = window.setInterval(poller, 30 * 1000); | ||
POLLs$1[id] = window.setInterval(poller, 30 * 1000); | ||
poller(); | ||
@@ -898,3 +948,149 @@ if (!_.get(k8sType, 'verbs', ['watch']).includes('watch')) { | ||
}; | ||
const receivedResources = (resources) => action(ActionType$1.ReceivedResources, { resources }); | ||
const getResourcesInFlight = () => action(ActionType$1.GetResourcesInFlight); | ||
const SDK_API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY = 'sdk/api-discovery-resources'; | ||
const cacheResources = (resources) => { | ||
try { | ||
localStorage.setItem(SDK_API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY, JSON.stringify(resources)); | ||
} | ||
catch (e) { | ||
consoleLogger.error('Error caching API resources in localStorage', e); | ||
throw e; | ||
} | ||
}; | ||
const getCachedResources = async () => { | ||
const resourcesJSON = localStorage.getItem(SDK_API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY); | ||
if (!resourcesJSON) { | ||
throw new Error(`No API resources found in localStorage for key ${SDK_API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY}`); | ||
} | ||
// Clear cached resources after load as a safeguard. If there's any errors | ||
// with the content that prevents the console from working, the bad data | ||
// will not be loaded when the user refreshes the console. The cache will | ||
// be refreshed when discovery completes. | ||
localStorage.removeItem(SDK_API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY); | ||
const resources = JSON.parse(resourcesJSON); | ||
consoleLogger.info('Loaded cached API resources from localStorage'); | ||
return resources; | ||
}; | ||
const POLLs = {}; | ||
const apiDiscovery = 'apiDiscovery'; | ||
const API_DISCOVERY_POLL_INTERVAL = 60000; | ||
const pluralizeKind = (kind) => { | ||
// Use startCase to separate words so the last can be pluralized but remove spaces so as not to humanize | ||
const pluralized = plural(_.startCase(kind)).replace(/\s+/g, ''); | ||
// Handle special cases like DB -> DBs (instead of DBS). | ||
if (pluralized === `${kind}S`) { | ||
return `${kind}s`; | ||
} | ||
return pluralized; | ||
}; | ||
const defineModels = (list) => { | ||
const { apiGroup, apiVersion } = list; | ||
if (!list.resources || list.resources.length < 1) { | ||
return []; | ||
} | ||
return list.resources | ||
.filter(({ name }) => !name.includes('/')) | ||
.map(({ name, singularName, namespaced, kind, verbs, shortNames }) => ({ | ||
...(apiGroup ? { apiGroup } : {}), | ||
apiVersion, | ||
kind, | ||
namespaced, | ||
verbs, | ||
shortNames, | ||
plural: name, | ||
crd: true, | ||
abbr: kindToAbbr(kind), | ||
labelPlural: pluralizeKind(kind), | ||
path: name, | ||
id: singularName, | ||
label: kind, | ||
})); | ||
}; | ||
const getResources = async () => { | ||
const apiResourceData = await commonFetchJSON('/apis'); | ||
const groupVersionMap = apiResourceData.groups.reduce((acc, { name, versions, preferredVersion: { version } }) => { | ||
acc[name] = { | ||
versions: _.map(versions, 'version'), | ||
preferredVersion: version, | ||
}; | ||
return acc; | ||
}, {}); | ||
const all = _.flatten(apiResourceData.groups.map((group) => group.versions.map((version) => `/apis/${version.groupVersion}`))) | ||
.concat(['/api/v1']) | ||
.map((p) => commonFetchJSON(`api/kubernetes${p}`).catch((err) => err)); | ||
return Promise.all(all).then((data) => { | ||
const resourceSet = new Set(); | ||
const namespacedSet = new Set(); | ||
data.forEach((d) => d.resources && | ||
d.resources.forEach(({ namespaced, name }) => { | ||
resourceSet.add(name); | ||
if (namespaced) { | ||
namespacedSet.add(name); | ||
} | ||
})); | ||
const allResources = [...resourceSet].sort(); | ||
const safeResources = []; | ||
const adminResources = []; | ||
const models = _.flatten(data.filter((d) => d.resources).map(defineModels)); | ||
const coreResources = new Set([ | ||
'roles', | ||
'rolebindings', | ||
'clusterroles', | ||
'clusterrolebindings', | ||
'thirdpartyresources', | ||
'nodes', | ||
'secrets', | ||
]); | ||
allResources.forEach((r) => coreResources.has(r.split('/')[0]) ? adminResources.push(r) : safeResources.push(r)); | ||
const configResources = _.filter(models, (m) => m.apiGroup === 'config.openshift.io' && m.kind !== 'ClusterOperator'); | ||
const clusterOperatorConfigResources = _.filter(models, (m) => m.apiGroup === 'operator.openshift.io'); | ||
return { | ||
allResources, | ||
safeResources, | ||
adminResources, | ||
configResources, | ||
clusterOperatorConfigResources, | ||
namespacedSet, | ||
models, | ||
groupVersionMap, | ||
}; | ||
}); | ||
}; | ||
const updateResources = () => (dispatch) => { | ||
dispatch(getResourcesInFlight()); | ||
getResources() | ||
.then((resources) => { | ||
// Cache the resources whenever discovery completes to improve console load times. | ||
cacheResources(resources); | ||
dispatch(receivedResources(resources)); | ||
return resources; | ||
}) | ||
.catch((err) => consoleLogger.error('Fetching resource failed:', err)); | ||
}; | ||
const startAPIDiscovery = () => (dispatch) => { | ||
consoleLogger.info('API discovery method: Polling'); | ||
// Poll API discovery every 30 seconds since we can't watch CRDs | ||
dispatch(updateResources()); | ||
if (POLLs[apiDiscovery]) { | ||
clearTimeout(POLLs[apiDiscovery]); | ||
delete POLLs[apiDiscovery]; | ||
} | ||
POLLs[apiDiscovery] = window.setTimeout(() => dispatch(startAPIDiscovery()), API_DISCOVERY_POLL_INTERVAL); | ||
}; | ||
const initAPIDiscovery = (storeInstance) => { | ||
getCachedResources() | ||
.then((resources) => { | ||
if (resources) { | ||
storeInstance.dispatch(receivedResources(resources)); | ||
} | ||
// Still perform discovery to refresh the cache. | ||
storeInstance.dispatch(startAPIDiscovery()); | ||
return resources; | ||
}) | ||
.catch(() => storeInstance.dispatch(startAPIDiscovery())); | ||
}; | ||
var ActionType; | ||
@@ -908,4 +1104,330 @@ (function (ActionType) { | ||
const emptyCoreState = { user: { identities: [], apiVersion: '', kind: '' }, activeCluster: '' }; | ||
/** | ||
* Reducer function for the core | ||
* @param state the reducer state | ||
* @param action provided associated action type alongwith payload | ||
* @param action.type type of the action | ||
* @param action.payload associated payload for the action | ||
* @see CoreAction | ||
* @return The the updated state. | ||
* * */ | ||
const coreReducer = (state, action) => { | ||
if (!state) { | ||
return emptyCoreState; | ||
} | ||
switch (action.type) { | ||
case ActionType.BeginImpersonate: | ||
return { | ||
...state, | ||
impersonate: { | ||
kind: action.payload.kind, | ||
name: action.payload.name, | ||
subprotocols: action.payload.subprotocols, | ||
}, | ||
}; | ||
case ActionType.EndImpersonate: | ||
return _.omit(state, 'impersonate'); | ||
case ActionType.SetUser: | ||
return { | ||
...state, | ||
user: action.payload.user, | ||
}; | ||
case ActionType.SetActiveCluster: | ||
return { | ||
...state, | ||
activeCluster: action.payload.cluster, | ||
}; | ||
default: | ||
return state; | ||
} | ||
}; | ||
const getReduxIdPayload = (state, reduxId) => state?.k8s?.get(reduxId); | ||
const getK8sDataById = (state, id) => state?.getIn([id, 'data']); | ||
const getQN = (obj) => { | ||
const { name, namespace } = obj.metadata || {}; | ||
return (namespace ? `(${namespace})-` : '') + name; | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const moreRecent = (a, b) => { | ||
const metaA = a.get('metadata').toJSON(); | ||
const metaB = b.get('metadata').toJSON(); | ||
if (metaA.uid !== metaB.uid) { | ||
return new Date(metaA.creationTimestamp) > new Date(metaB.creationTimestamp); | ||
} | ||
return parseInt(metaA.resourceVersion, 10) > parseInt(metaB.resourceVersion, 10); | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const removeFromList = (list, resource) => { | ||
const qualifiedName = getQN(resource); | ||
consoleLogger.info(`deleting ${qualifiedName}`); | ||
return list.delete(qualifiedName); | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const updateList = (list, nextJS) => { | ||
const qualifiedName = getQN(nextJS); | ||
const current = list.get(qualifiedName); | ||
const next = fromJS(nextJS); | ||
if (!current) { | ||
return list.set(qualifiedName, next); | ||
} | ||
if (!moreRecent(next, current)) { | ||
return list; | ||
} | ||
// TODO: (kans) only store the data for things we display ... | ||
// and then only do this comparison for the same stuff! | ||
if (current | ||
.deleteIn(['metadata', 'resourceVersion']) | ||
.equals(next.deleteIn(['metadata', 'resourceVersion']))) { | ||
// If the only thing that differs is resource version, don't fire an update. | ||
return list; | ||
} | ||
return list.set(qualifiedName, next); | ||
}; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const loadList = (oldList, resources) => { | ||
const existingKeys = new Set(oldList.keys()); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return oldList.withMutations((list) => { | ||
(resources || []).forEach((r) => { | ||
const qualifiedName = getQN(r); | ||
existingKeys.delete(qualifiedName); | ||
const next = fromJS(r); | ||
const current = list.get(qualifiedName); | ||
if (!current || moreRecent(next, current)) { | ||
list.set(qualifiedName, next); | ||
} | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
existingKeys.forEach((k) => { | ||
const r = list.get(k); | ||
const metadata = r.get('metadata').toJSON(); | ||
if (!metadata.deletionTimestamp) { | ||
consoleLogger.warn(`${metadata.namespace}-${metadata.name} is gone with no deletion timestamp!`); | ||
} | ||
list.delete(k); | ||
}); | ||
}); | ||
}; | ||
const sdkK8sReducer = (state, action) => { | ||
if (!state) { | ||
return fromJS({ | ||
RESOURCES: { inFlight: false, models: Map() }, | ||
}); | ||
} | ||
let newList; | ||
switch (action.type) { | ||
case ActionType$1.GetResourcesInFlight: | ||
return state.setIn(['RESOURCES', 'inFlight'], true); | ||
case ActionType$1.ReceivedResources: | ||
return (action.payload.resources.models | ||
.filter((model) => !state?.getIn(['RESOURCES', 'models']).has(getReferenceForModel(model))) | ||
.filter((model) => { | ||
const existingModel = state?.getIn(['RESOURCES', 'models', model.kind]); | ||
return (!existingModel || getReferenceForModel(existingModel) !== getReferenceForModel(model)); | ||
}) | ||
.map((model) => { | ||
if (model.namespaced) { | ||
getNamespacedResources().add(getReferenceForModel(model)); | ||
} | ||
else { | ||
getNamespacedResources().delete(getReferenceForModel(model)); | ||
} | ||
return model; | ||
}) | ||
.reduce((prevState, newModel) => { | ||
// FIXME: Need to use `kind` as model reference for legacy components accessing k8s primitives | ||
const [modelRef, model] = allModels().findEntry((staticModel) => staticModel | ||
? getReferenceForModel(staticModel) === getReferenceForModel(newModel) | ||
: false) || [getReferenceForModel(newModel), newModel]; | ||
// Verbs and short names are not part of the static model definitions, so use the values found during discovery. | ||
return prevState.updateIn(['RESOURCES', 'models'], (models) => models.set(modelRef, { | ||
...model, | ||
verbs: newModel.verbs, | ||
shortNames: newModel.shortNames, | ||
})); | ||
}, state) | ||
// TODO: Determine where these are used and implement filtering in that component instead of storing in Redux | ||
.setIn(['RESOURCES', 'allResources'], action.payload.resources.allResources) | ||
.setIn(['RESOURCES', 'safeResources'], action.payload.resources.safeResources) | ||
.setIn(['RESOURCES', 'adminResources'], action.payload.resources.adminResources) | ||
.setIn(['RESOURCES', 'configResources'], action.payload.resources.configResources) | ||
.setIn(['RESOURCES', 'clusterOperatorConfigResources'], action.payload.resources.clusterOperatorConfigResources) | ||
.setIn(['RESOURCES', 'namespacedSet'], action.payload.resources.namespacedSet) | ||
.setIn(['RESOURCES', 'groupToVersionMap'], action.payload.resources.groupVersionMap) | ||
.setIn(['RESOURCES', 'inFlight'], false)); | ||
case ActionType$1.StartWatchK8sObject: | ||
return state.set(action.payload.id, Map({ | ||
loadError: '', | ||
loaded: false, | ||
data: {}, | ||
})); | ||
case ActionType$1.StartWatchK8sList: | ||
if (getK8sDataById(state, action.payload.id)) { | ||
return state; | ||
} | ||
// We mergeDeep instead of overwriting state because it's possible to add filters before load/watching | ||
return state.mergeDeep({ | ||
[action.payload.id]: { | ||
loadError: '', | ||
// has the data set been loaded successfully | ||
loaded: false, | ||
// Canonical data | ||
data: Map(), | ||
// client side filters to be applied externally (ie, we keep all data intact) | ||
filters: Map(), | ||
// The name of an element in the list that has been "selected" | ||
selected: null, | ||
}, | ||
}); | ||
case ActionType$1.ModifyObject: { | ||
const { k8sObjects, id } = action.payload; | ||
let currentJS = getK8sDataById(state, id) || {}; | ||
// getIn can return JS object or Immutable object | ||
if (currentJS.toJSON) { | ||
currentJS = currentJS.toJSON(); | ||
currentJS.metadata.resourceVersion = k8sObjects?.metadata?.resourceVersion; | ||
if (_.isEqual(currentJS, k8sObjects)) { | ||
// If the only thing that differs is resource version, don't fire an update. | ||
return state; | ||
} | ||
} | ||
return state.mergeIn([id], { | ||
loadError: '', | ||
loaded: true, | ||
data: k8sObjects, | ||
}); | ||
} | ||
case ActionType$1.StopWatchK8s: | ||
return state.delete(action.payload.id); | ||
case ActionType$1.Errored: | ||
if (!getK8sDataById(state, action.payload.id)) { | ||
return state; | ||
} | ||
/* Don't overwrite data or loaded state if there was an error. Better to | ||
* keep stale data around than to suddenly have it disappear on a user. | ||
*/ | ||
return state.setIn([action.payload.id, 'loadError'], action.payload.k8sObjects); | ||
case ActionType$1.Loaded: | ||
if (!getK8sDataById(state, action.payload.id)) { | ||
return state; | ||
} | ||
consoleLogger.info(`loaded ${action.payload.id}`); | ||
// eslint-disable-next-line no-param-reassign | ||
state = state.mergeDeep({ | ||
[action.payload.id]: { loaded: true, loadError: '' }, | ||
}); | ||
newList = loadList(getK8sDataById(state, action.payload.id), action.payload.k8sObjects); | ||
break; | ||
case ActionType$1.UpdateListFromWS: | ||
newList = getK8sDataById(state, action.payload.id); | ||
// k8sObjects is an array of k8s WS Events | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const { type, object } of action.payload.k8sObjects) { | ||
switch (type) { | ||
case 'DELETED': | ||
newList = removeFromList(newList, object); | ||
break; | ||
case 'ADDED': | ||
case 'MODIFIED': | ||
newList = updateList(newList, object); | ||
break; | ||
default: | ||
// possible `ERROR` type or other | ||
consoleLogger.warn(`unknown websocket action: ${type}`); | ||
} | ||
} | ||
break; | ||
case ActionType$1.BulkAddToList: | ||
if (!getK8sDataById(state, action.payload.id)) { | ||
return state; | ||
} | ||
newList = getK8sDataById(state, action.payload.id); | ||
newList = newList.merge(action.payload.k8sObjects.reduce((map, obj) => map.set(getQN(obj), fromJS(obj)), Map())); | ||
break; | ||
case ActionType$1.FilterList: | ||
return state.setIn([action.payload.id, 'filters', action.payload.name], action.payload.value); | ||
default: | ||
return state; | ||
} | ||
return state.setIn([action.payload.id, 'data'], newList); | ||
}; | ||
/** | ||
* Dynamic Plugin SDK Redux store reducers | ||
* | ||
* If the app uses Redux, these can be spread into the root of your store to provide an integrated SDK. | ||
* If the app does not use Redux, these will be provided via the SDK Redux Store. | ||
*/ | ||
const SDKReducers = Object.freeze({ | ||
sdkCore: coreReducer, | ||
k8s: sdkK8sReducer, | ||
}); | ||
/** | ||
* `useReduxStore` will provide the store instance if present or else create one along with info if the context was present. | ||
* | ||
* @example | ||
* ```ts | ||
* function Component () { | ||
* const {store, storeContextPresent} = useReduxStore() | ||
* return ... | ||
* } | ||
* ``` | ||
*/ | ||
const useReduxStore = () => { | ||
const storeContext = useStore(); | ||
const [storeContextPresent, setStoreContextPresent] = React.useState(false); | ||
const store = React.useMemo(() => { | ||
if (storeContext) { | ||
setStoreContextPresent(true); | ||
setReduxStore(storeContext); | ||
} | ||
else { | ||
consoleLogger.info('Creating the SDK redux store'); | ||
setStoreContextPresent(false); | ||
const storeInstance = createStore(combineReducers(SDKReducers), {}, compose(applyMiddleware(thunk))); | ||
setReduxStore(storeInstance); | ||
} | ||
return getReduxStore(); | ||
}, [storeContext]); | ||
return { store, storeContextPresent }; | ||
}; | ||
/** | ||
* Initializes the host application to work with Kubernetes and related SDK utilities. | ||
* Add this at app-level to make use of app's redux store and pass configurations prop needed to initialize the app, preferred to have it under Provider. | ||
* It checks for store instance if present or not. | ||
* If the store is there then the reference is persisted to be used in SDK else it creates a new store and passes it to the children with the provider | ||
* @component AppInitSDK | ||
* @example | ||
* ```tsx | ||
* return ( | ||
* <Provider store={store}> | ||
* <AppInitSDK configurations={{ appFetch, pluginStore, wsAppSettings }}> | ||
* <App /> | ||
* </AppInitSDK> | ||
* </Provider> | ||
* ) | ||
* ``` | ||
*/ | ||
const AppInitSDK = ({ children, configurations }) => { | ||
const { store, storeContextPresent } = useReduxStore(); | ||
const { appFetch, pluginStore, wsAppSettings, apiDiscovery = initAPIDiscovery } = configurations; | ||
React.useEffect(() => { | ||
try { | ||
if (isUtilsConfigSet()) { | ||
setUtilsConfig({ appFetch, wsAppSettings }); | ||
} | ||
apiDiscovery(store); | ||
} | ||
catch (e) { | ||
consoleLogger.warn('Error while initializing AppInitSDK', e); | ||
} | ||
}, [apiDiscovery, appFetch, store, wsAppSettings]); | ||
return (React.createElement(PluginStoreProvider, { store: pluginStore }, !storeContextPresent ? React.createElement(Provider, { store: store }, children) : children)); | ||
}; | ||
class NoModelError extends CustomError { | ||
@@ -1082,2 +1604,2 @@ constructor() { | ||
export { WebSocketFactory, WebSocketState, commonFetch, commonFetchJSON, commonFetchText, getK8sResourceURL, getUtilsConfig, isUtilsConfigSet, k8sCreateResource, k8sDeleteResource, k8sGetResource, k8sListResource, k8sListResourceItems, k8sPatchResource, k8sUpdateResource, setUtilsConfig, useK8sWatchResource }; | ||
export { AppInitSDK, WebSocketFactory, WebSocketState, commonFetch, commonFetchJSON, commonFetchText, getK8sResourceURL, getUtilsConfig, isUtilsConfigSet, k8sCreateResource, k8sDeleteResource, k8sGetResource, k8sListResource, k8sListResourceItems, k8sPatchResource, k8sUpdateResource, setUtilsConfig, useK8sWatchResource }; |
{ | ||
"name": "@openshift/dynamic-plugin-sdk-utils", | ||
"version": "1.0.0-alpha3", | ||
"version": "1.0.0-alpha4", | ||
"description": "Provides Kubernetes and React utilities", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
82322
2062
0