-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Remove special handling of savedObjectId
in embeddables and containers
#63139
Comments
Pinging @elastic/kibana-app (Team:KibanaApp) |
i partly agree with Stacey here. i think if embeddable is backed by saved object, then indeed the consumer should not care about that and should just provide the right input. HOWEVER: i think in general embeddables should not be backed by saved objects, for example visualize embeddable should receive visState on the input and not savedObjectId. Embeddable should not care where this visState comes from, so you can instantiate it from saved object, from some state in memory or whatever else. Embeddable internally should not use savedObjectLoader, but just work with the state given. To make the case where we actually have saved objects that back embeddables (saved visualization) the associated embeddable factory should provide you with easy way of loading the saved object and giving you back an embeddable which state is based on the saved object. In this case then the consumer needs to know if it wants to create embeddable from some state it already has or if it wants to load some saved object to get the state it needs to create the embeddable. (the consumer in our case being dashboard) this keeps embeddables (visualize for example) simple(r), (no need to do loading of saved objects and conditional handling of input) and with less dependencies (no dependency on saved object client for one). this does require some additional handling in consumer (thats how it works today by the way), as consumer needs to be aware if its panel state contains the embeddable state or saved object id .... but imo thats the correct way of looking at it, its the dashboard that has the capability to have panels which link to other saved objects and panels which contain state by value. |
I've already approved Stacey's PR here and I don't have much to add. The approach Stacey is taking seems perfectly fine to me - there is already a layer of abstraction, with the consumer calling just Changing this to a way Peter describes seems like a massive change in the way embeddables work, but I trust you have more context on this. I would be very curious to understand further reasons for considering this approach. From what I can tell, the changes that will required in the dashboard code to support this outweighs any benefits you may get from this "cleaner", more abstract approach. (Eg. is there a plan to add other embeddables not backed up by a saved object any time soon? To my knowledge, this will only be the case for visualizations in the dashboard for the forseeable future). |
actually, if i am not missing something what i am proposing is closer to the current way embeddables work (or pretty much what we have at the moment) |
As I understood from the meeting, we have two main approaches for allowing embeddables to be created via 1: the logic can stay within the domain of the dashboard / container, which means that the embeddable / factory does not care where its state comes from, with the disadvantage that all embeddables will be treated the same way. Additionally, transferring an embeddable between being stored as a reference and a value will have to be re-thought. 2: the logic can be stored in the embeddable factory. This has the advantage of removing the special treatment of savedobjectid by the container, and the advantage of allowing each embeddable factory to decide its own logic for instantiation. With this approach we would have to be careful to avoid duplication of effort, and we would have to be aware of special cases like when a I am leaning towards option 2 because it gives embeddable authors more freedom to decide what to do with the input provided to them. Please let me know if I've misrepresented or misunderstood anything! |
Going to summarize my response/feelings from this mornings meeting. What are we deciding between?Option 1: Saved objects as a formal concept in the Embeddable system, with dedicated APIs and function calls. This is how it is today. Special considerations for each approach.Containers deserializing childrenGeneric or specific "save to library" actionOption 1:
Option 1 code would look something like: // Generic "save to library" function with saved objects as an formal
// concept on embeddable factories.
createSaveToLibraryAction = ({ factories, savedObjectClient }) => createAction<{ embeddable }> => ({
displayName: 'Save To Library'
isCompatible: ({ embeddable }) => (
// This factory supports saved objects
factories.get(embeddable.type).savedObjectMetadata &&
// We need to check if this embeddable is already stored as "by reference"
// on it's container. Going this route, only embeddables rendered inside containers
// can be "saved to library". Embeddables rendered individually, outside of a container,
// could not support this functionality because the container holds the knowledge of
// where state came from.
embeddable.getRoot().getPanelForChild(embeddable.id).savedObjectId === undefined
),
execute: ({ embeddable }) => {
// ?? How do we ensure there is an exact mapping of input => saved object attributes?
// Other ideas here?
const savedObjectAttributes = embeddable.getInput();
const id = savedObjectClient.create(embeddable.type, savedObjectAttributes);
embeddable.getRoot().getPanelForChild(embeddable.id).savedObjectId = id;
// ?? This wouldn't really work...
// Not sure what to do here in a generic fashion. Some data here could be
// "overrides" that should stay, like if this panel had a "per panel time range"
// set on it. But other data could have been retrieved from the user during the
// `factory.getExplicitInput` function and stored here, like if this was a
// SayHelloEmbeddable that asked for the users name, and rendered "Hello ${name}",
// this name value would need to be wiped out to ensure the embeddable was rendering
// the attributes all from the savedObjectId. The container doesn't know the difference
//
// I think this is pointing to the reason that the container should not be responsible
// for handling this generically.
embeddable.getRoot().getPanelForChild(embeddable.id).explicitInput = {};
}
}); This would be a difficult feature to implement for saved searches, maps, in one go, because there is no generic way to get the savedObjectAttributes from an embeddable in a generic fashion. We'd have to probably make sure we excluded them from supporting this initially. Option 2 // Generic "save to library" function with saved objects as an internal
// detail on embeddables, not factories.
createSaveToLibraryAction = ({ factories }) => createAction<{ embeddable }> => ({
displayName: 'Save To Library'
isCompatible: ({ embeddable }) => (
// This factory supports saved objects
factories.get(embeddable.type).savedObjectMetadata &&
// This embeddable is not already backed by a saved object.
embeddable.getInput().savedObjectId === undefined,
execute({ embeddable }) => {
// Again if we are trying to support this generically, we need to somehow make sure
// we can extract savedObjectAttribtues from input somehow.
const savedObjectAttributes = embeddable.getInput().attributes;
const id = savedObjectClient.create(embeddable.type, savedObjectAttributes);
embeddable.updateInput({ savedObjectId: id, attributes: undefined });
// Pros here to above version: embeddable doesn't care whether it's being rendered
// inside a container or by itself. Action still works.
}
}) However, if we didn't want to make this generic at all in the first go, the kibana app team could register their own action specific to visualize embeddables: // Specific "save to library" implementation.
createSaveVisToLibraryAction = ({ factories }) => createAction<{ embeddable: VisEmbeddable }> => ({
displayName: 'Save To Library'
isCompatible: ({ embeddable }) => (
// This embeddable is not already backed by a saved object.
// Note this is specific to this embeddable so we can use whatever name here we like.
embeddable.getInput().visObjectId === undefined,
execute({ embeddable }) => {
// I wrote this embeddable, I know I put the attributes here, I can do this:
const attributes = embeddable.getInput().attributes;
const id = savedObjectClient.create(embeddable.type, attributes);
embeddable.updateInput({ visObjectId: id, attributes: undefined });
// Very similar to above version but I have more flexibility with storage.
// I don't _have_ to store saved object data in input.attributes, I could
// derive it and clear out what I want.
// Pros: More flexible to the author of the embeddable.
// Cons: I have to write and maintain this code.
// I personally think this is the best way right now and that
// creating an abstraction right now would be premature.
}
}) Add panel flowoption 1 continues as it is now, conditionally calling either pseudocode: // Today
function addPanelFlyout() {
const addPanelFromSavedObject = (savedObjectId: string, type: string) => {
// Forcing a 1:0-1: mapping of factory to saved object type.
const factory = findFactorySupportingType(type);
const panel = factory.createBySavedObjectId(savedObjectId);
}
return (
<SavedObjectFinder addFunction={addPanelFromSavedObject} />
<CreateNewButton />
);
} Option2 Still not my favorite implementation, but improved to use only the single create function on the factory: function addPanelFlyout() {
const addPanelFromSavedObject = (savedObjectId: string, type: string) => {
// Forcing a 1:0-1: mapping of factory to saved object type.
const factory = findFactorySupportingType(type);
// I don't like this either! How do we know this embeddable doesn't need any
// additional information along with `savedObjectId`? We are still treating it
// like a special concept here.
const panel = factory.create({ savedObjectId });
}
return (
<SavedObjectFinder addFunction={addPanelFromSavedObject} />
<CreateNewButton />
);
} However if we were to truly decouple embeddables from saved objects and completely make them an implementation detail, it would look like this: // In an ideal world
function addPanelFlyout() {
const renderFactoryOption(factory => <FactoryOption onClick={() => {
// This function on the factory is in charge of requesting whatever data
// it needs from the user to get input data - in the "backed by saved object world", that
// would be showing the user a list of saved objects to choose from. In the "by value" world,
// that would be an editor.
// Note that this could be a totally different registry of functions.
// This would be tough to implement though because we'd have to change the design and not use
// the SavedObjectFinder anymore.
//
// Bonus - multiple factories could support the same saved object type.
//
// You could do this still today, but the UI flow is awkward, it's through the "create new"
// button.
const input = factory.getExplicitInput();
container.addChild(factory.create(input));
}} />
})
return factories.forEach(renderFactoryOption);
} There are no longer two paths in the add panel flyout - We could also use a separate registry that is responsible for asking the user for the data necessary to create an instance of an embeddable. However, right now this is captured with the This change would require a change to the UX design however. I think we will want to change this anyway, since the "create new embeddable" flow is hidden, especially with the latest dashboard changes. There are three Create news hooked into the visualize extension point: Create new hooked into embeddable factory creation: SummaryThe goal of this PR is to make
is perfectly acceptable (and how almost all of the example embeddables work).
|
I'd like to call out another issue with factories converting If the embeddable.getRoot()?.getPanelForChild(embeddable.id).savedObjectId which means these actions only work on embeddables inside a container.
They want functionality to support editing the dashboard (by reference) inside the SIEM app and saving edits. (#52680 (comment)). How would a save action work for a "dashboard by reference", when only the factory has the saved object id upon creation time, and the instance of the embeddable does not? |
I appreciate the discussion here, and that there is no absolute correct answer. There’s a trade off between keeping Embeddables simpler with no savedObject dependency, and giving authors leeway. Stacey laid out the case and rationale, it makes sense to me I think we should align and proceed with pushing responsibility of getting state to the embeddable author. The arguments of simplicity and reduced dependencies are valid but I'm not seeing a huge downside. Unless I'm missing something big can we align and close this off @ppisljar @stacey-gammon |
i didn't read whole stacey's response yet, but i want to raise this again: |
I agree with the conclusion here and the approach seems perfectly reasonable to me. I just have a general suggestion for these discussions in the future: |
It's not just aesthetic, it's needed to support functionality like save and edit on embedded dashboards by reference. I'm going to move forward with the PR, as a decision needs to be made here, and Peter S. weighed in, in support. @majagrubic - agree. I was waiting on Peter P.s response to help me understand what exactly he didn't like in order to form a response. This came the morning of the meeting so there was time for offline discussion in prep. I agree it's better to get the sides of the argument out before a meeting so there is time for everyone to form an opinion with full knowledge. Thanks everyone for weighing in and contributing to this discussion! |
Pinging @elastic/kibana-app-arch (Team:AppArch) |
This PR removes the special handling of embeddables backed by saved objects vs those that are not backed by saved objects.
From the description of the PR:
PanelState
fromto
addSavedObjectEmbeddable
from the container interface. Now the functionaddNewEmbeddable
can be used for all cases.A follow up will be to remove the
factory.createFromSavedObject
function as that should not be needed anymore.I believe that whether or not an embeddable is backed by a saved object should be an implementation detail of that embeddable and not require any external knowledge or special handling. As shown in the PR there is no need for this.
I think code like:
Should be replaced by:
@ppisljar has expressed concerns about this approach, filing here so we can discuss.
The text was updated successfully, but these errors were encountered: