@os1-platform/console-ui-react
v1.5.0
Published
React wrapper for console-ui library
Downloads
377
Maintainers
Keywords
Readme
@os1-platform/console-ui-react
Installation and Usage
How to use?
Install console-ui-react into your project.
npm install @os1-platform/console-ui-react
Use
OS1Provider
function of the library to use aunthentication and render header and sidebar. Pass function as a prop to your application that has setState method which sets the state of console Ui Instance.import { OS1Provider } from '@os1-platform/console-ui-react'; <OS1Provider clientId={`sampleClientId`} // This is the clientId that you get after creating an app. loginRedirectPath={'/abc'} // path that will be given when someone logins into console ui for the first time logoutRedirectPath={'/'} //path that needs to be redirected when someone logouts from your application. devTenantId={'tenantId'} // this is an optional parameter, need only to be passed when using in development mode. appId={'SolutionName-appId'} //initial appId along with solution name on which you want to land when loading console ui for the first time. This is mandatory prop. > <Initiate setConsole={handleConsoleInstanceChange} /> //This is your component, which is needed to set Instance of Console ui. </OS1Provider>;
Create Any Component that uses
ConsoleUiContext
and sets that as a state to be used by client application.import React from 'react'; import { ConsoleUIContext } from '@os1-platform/console-ui-react'; function Initiate(props) { const consoleInstance = React.useContext(ConsoleUIContext); if (consoleInstance) { props.setConsole(consoleInstance); } } export default Initiate;
We can also provide injectable controls to our header and sidebar by passing them as props to OS1Provider.
import { OS1Provider } from "@os1-platform/console-ui-react"; const controls = [{ type: "AutoComplete", //Type can be TextBox,TextBoxButton,DropDown, SearchBox, AutoComplete width: 100, // width as percentage of maximum width that is assigned to injectable component placeholder: "Search Items", // placeHolder text id: "AutoComplete1", //Unique Id which distinguish it with other injectable controls. float: "left", // Option to align items left or right functionBoundOption: autoComplete(), // Function that return value as an array of object in the form of [{ value: string, text: string }] }] <OS1Provider clientId={`sampleClientId`} loginRedirectPath={"/abc"} logoutRedirectPath={"/"} devTenantId={"tenantId"} controls ={controls} appId={'SolutionName-appId'}> <Initiate setConsole={handleConsoleInstanceChange} /> </OS1Provider>
NOTE: There can not be more than 3 injectable controls and each injectable control should be given a unique id.
Use the Toast function to render toast component in your webpage. For example:
const toastConfig = { bgColor: "yellow", // background color of the toast [green,red,yellow,black,white] message: "Operation Successful", // message that will be shown inside the toast [128 character limit] timeout: 10, // time in seconds after which toast will disappear icon: "error", // icon that will be appearing before the message [info,error,warning,success,failure,none] closeButton: true, // denotes whether the close button will be present on the right of the toast } <OS1Toast elementId={"toastElement"} toastConfig={toastConfig} />
NOTE: Each toast element that is to be rendered on the page should be given a unique elementId.
Use the Modal function to render a modal component in your webpage. For example:
const modalConfig = { title: 'Deactivate', // Title of the modal [96 characters limit] message: 'Do you want to continue?', // message that will be appearing on the modal icon: 'info', // icon that will be appearing along with the message [info,error,warning,success,failure,none] buttons: [ // maximum two buttons allowed { id: 'button-element-1', // unique id of the button backgroundColor: 'green', // background color of the button text: 'Cancel', // text appearing on the button event: 'upEvent', // unique name of the custom event that will be triggered on click }, ], }; <OS1Modal elementId={'modalElement'} modalConfig={modalConfig} />;
Note:- Listen to event when button is clicked,
event.details
will contain the modal element Id.Use state variable of
ConsoleUIContext
context to listen to events emitted by injectable controls. This instance is usually passed as a prop to apps. For example:const { consoleInstance } = props; if (consoleInstance) { consoleInstance .eventBus() .on(consoleInstance.events().OnChangeEvent, (e) => window.console.log(e) ); }
Note:- We are exposing OnChangeEvent, OnBlurEvent, OnScrollEvent, OnClickEvent, OnFocusEvent, OnSearchEvent for injectableId's.
Use
OS1HttpClient
API to create a client variable for network requests and passauthInitializer
property of ConsoleUiContext as a constructor parameter.import { OS1HttpClient } from '@os1-platform/console-ui-react'; if (consoleInstane) { var client = new OS1HttpClient( consoleInstane.authInitializer, `https://abc.domain.com` ); // Base url is set in OS1HttpClient class. }
Use REST api methods, on the instance created for OS1HttpClient. Following headers are automatically configured to requests originating from the
NetworkClient
adding Access token(x-coreos-access
) or Tenant id(x-coreos-tid
) or User info(x-coreos-userinfo
) or Auth token(x-coreos-auth
) headers to the actual request.withAccess
withTid
withUserInfo
withAuth
Note:
- By default all these headers are true, pass value against these headers as
false
to remove from request. - Access token is verified and regenerated (if expired), every time an api request is made.
x-coreos-userinfo
contains the userId.x-coreos-auth
contains the id_token.- If you want to use
X-Coreos-Request-Id
, that is generated by library and also need to send request headers, then send requestId parameter asundefined
.
const handleClick = () => { //here consoleInstance is the state variable of ConsoleUIContext; if (consoleInstance) { const client = new OS1HttpClient( consoleInstance.authInitializer, `https://abc.domain.com` ); //consoleInstance.authInitializer is an instance of AAA class that we get from ConsoleUi Context const reqHeaders: any = { withAccess: false, withTid: false, withUserInfo: false, withAuth: false, }; client .get(`/users`) .then((response) => { console.log('api response', response.data); }) .catch(function (err) { console.error('error', err); }); // this example covers case when requestId is generated from console and requestHeaders are passed client .post(`/todos`, { todo: 'study' }, undefined, reqHeaders) .then((response) => { console.log('api response', response.data); }) .catch(function (err) { console.error('error', err); }); } };
Click on Need Help button in case you want to query about any application or want to use product guide.
- Click on
Raise a Ticket
button and Use Contact Us page, if you want to query about any application.- We provide text area to describe the issue in details.
- We provide option to upload screenshots if any, that may help to understand the issue.
- We will get the query along with your details and we can contact you.`
- Click on
Product Guide
button to learn about the product.
Option to change the timezone of the application. At initital load, time zone will be tenant specific but user can change the time zone according to his choice and it will be stored in his browser's local Storage.
Exposed
convertTime
function that converts time of the user's application in their desired format andcurrentTimeZone
function that gives user current timezone that is stored in the browser. When TimeZone is updated it emitsUpdateTimeZoneEvent
event.const { consoleInstance } = props; if (consoleInstance) { console.log( consoleInstance.convertTime('2014-06-01 12:00', 'MM-DD-YYYY HH:mm:ss') ); // In this 2nd parameter i.e. format in which we want to convert time is optional. console.log(consoleInstance.currentTimeZone()); }
We have provided that defines how user navigates on Routing of tabs. Users can choose, how they want to navigate apps i.e. they want to open the app on new tab or the current tab. This is done by going into user preference in profile dropdown and select the choice in Set App Redirect Behaviour. For the first time, it will ask how, we want to redirect our apps.
When user clicks on subroutes, then event naming
changeRoutes
is emitted, which contains the details of subMenu apps and their parent app details like appId, appUrl and appName.If nothing is selected then, when user clicks on app for the first time, it asks how we want to redirect the app.
To get Access Token, use
getAuthenticationTokens
method, for userInfo, usegetUser
, and check whether user is authenticated useisAuthenticated()
. User have to use context of consoleUi and call authInitializer method of it.getTenantConfigurations
method, to get all the organization based tenant data.
if (consoleInstance) {
const accessToken = await consoleInstance.authInitializer.getAuthenticationTokens();
const userInfo = await consoleInstance.authInitializer.getUser();
const isUserAuthenticated = await consoleInstance.authInitializer.isAuthenticated();
const tenantInfo = await consoleInstance.authInitializer.getTenantConfigurations();
}
Link to acceess example app
Integration guidelines for Rest Api Calls Using console UI Axios Client:-
Console has already exposed the
OS1HttpClient
class that accepts all HTTP methods.Wherever the user wants to send a callback url in their api call, they have to mention it like a placeholder naming
{{SSE_CALLBACK}}
. Console will look for this placeholder and replace it with a callback url.Internally this function will establish the sse connection and obtain the callback url from the server. Application developers just need to provide the parameter in api request where the callback url needs to be set.
Having intertab communication: The SSE console will make one connection per browser per user agent. If it is not enabled, it will make a new connection for each tab. All events will initially be sent to the master tab, which is the tab that has established the SSE connection. After that, the events will be shared with other tabs.
Broadcasting to different users: Each tab can subscribe to the topic(s). Any message broadcasted to topic(s) will be delivered to their subscribers.
Enabling inter-tab communication:-
To enable inter-tab communication, set the interTabCommunicationRequired flag to true, while initializing the console UI. if not set, it defaults to false.
<OS1Provider
clientId={process.env.REACT_APP_CLIENT_ID}
loginRedirectPath={'/redirectPath'}
logoutRedirectPath={'/logoutPath'}
appId={process.env.REACT_APP_INITIAL_APP_ID}
controls={controls}
interTabCommunicationRequired={true} // here it is set as true
>
<Initiate setConsole={handleConsoleInstanceChange} />
</OS1Provider>
- Console will make an api call by intercepting the request. Once callback is received from the server, the callback data is sent to the UI as a custom event named:
SSECallBackEvent
// here consoleInstance is an instance that is being created for consoleUI Context while initialization of console UI
document.addEventListener(
`${consoleInstance.events().SSECallBackEvent}`,
(eventData) => {
// This event Data can be used further as per the usage of app
}
);
- Users can set the callback url in their url path, query, payload or in headers.
- If any internal error is there while making a connection, It will be thrown back as an error and an api call will be made with null as callback url.
- Retriable errors like re-establishing connection will not be thrown as those will be retried internally. Only fatal errors along with api errors are thrown in following format:-
"error": {
"code": "200101",
"description": "There is a problem in SSE connection from server end"
},
"status": 503
Example to implement this mentioned below.
- Users can pass the placeholder for callback url as a parameter, body or header in an api and from there a message can be sent.
const dto = getDtoFromDisplay(data);
dto['callback'] = '{{SSE_CALLBACK}}';
// here client is the context of consoleUI that is set at the time of initialization.
const axiosClient = new OS1HttpClient(
client.authInitializer,
`${process.env.REACT_APP_BASE_URL}`
);
try {
await axiosClient.post(
'/vehicles',
Dto, // payload for api
'createVehicles',
{ withAuth: false }, // request headers for not sending token headers, this optional parameter
{
headers: {
// custom headers passed, this is optional parameter
'Callback': '{{SSE_CALLBACK}}',
'Content-Type': 'application/json',
},
}
);
return;
} catch (error) {
console.error('error', error);
}
- Here, in the above mentioned example
{{SSE_CALLBACK}}
, is passed in payload as well as in custom headers. Console Will replace this variable from the callback url and make an api call. - When msg is received on the client side, an event naming SSECallBackEvent is emitted and the user can listen to this event using document.addEventListener.
// here consoleInstance is an instance that is being created for consoleUI Context while initialization of console UI
document.addEventListener(`${consoleInstance.events().SSECallBackEvent}`, (eventData) => {
// This event Data can be used further as per the usage of app
})
// For better performance use removeEventListener to clean up this event listener.
Approach for rest and graphql api both-
- Console has exposed OS1HttpClient to make api calls. To use Server Side Events in apps, users need to instantiate the OS1HttpClient class. Even if they make their api calls with any other way instead of console axios client.
const cl = new OS1HttpClient(props.console.authInitializer, 'https://abc.sandbox.getos1.com');
// here, the first parameter is the console UI context and the second parameter is the domain url for which api call is to be made
- In
OS1HttpCLien
t, we have exposed thegetEventBrokerUrl
function to make Server side connection(example is added below). This function returns the callback url that has been established after successful connection establishment. When the callback url is already present, it will just increase the timer for the sliding window so that connection will persist longer for that particular url. - A callback url returned from the function, can be set in a state, to be used in the api call. - For optimized behavior, can use UseEffect hook to optimize the update of state. When the api needs to be triggered, users can check whether the callback url that is present in their state is different from the callback url received from the callback function. If it's different, then just update the state and return, else just call the api with the callback url that is stored in state. - Another approach is, users can maintain their global state or context, and store the callback url in it. Then whenever they need to make an api call just pass that context or state in their api. But they need to make sure that the getEventBrokerUrl function is called before an api call as it will always guarantee that the callback url is still active. Example:-
const [ callBackUrl, setCallBackUrl] = useState(null)
useEffect(()=>{
callEvent()
}, [callBackUrl])
if (props.console) { //consoleInstance that is set at console Initialization
var cl = new OS1HttpClient(props.console.authInitializer,
'https://os1devs.sandbox.getos1.com');
}
//This function is called whenever user want to make an api call
const handleHandshake = async () => {
const callbackResponse = await cl.getEventBrokerUrl()
if (callBackUrl && callbackResponse.callback === callBackUrl){
callEvent()
} else {
setCallBackUrl(callbackResponse.callback)
}
}
//Sample function to make an api call with generated callback url
function callEvent(){
if (props.console) {
cl.post(`/core/api/v1/aaa/apps`,{
properties: {"availability":true,"fuelType":"CNG","operatorId":"op-1" },
callBackUrl} // payload passed
)
.then(response => {
console.log("api response", response.data)
})
.catch(function (err) {
console.error('error in api call', err);
});
}
}
Here, in the above example:- - When the user calls handleHandshake function, then a call to establish connection is made and a callback url is received. - This callback url is being set to a state and can be used in an api call as a path, query, header or as a body. - After an api call is made and a message can be sent to that callback url from the backend application and UI application can listen to those messages via SSECallBackEvent
that is exposed:
// here consoleInstance is an instance that is being created for consoleUI Context while initialization of console UI
document.addEventListener(`${consoleInstance.events().SSECallBackEvent}`, (eventData) => {
// This event Data can be used further as per the usage of app
})
// For better performance use removeEventListener to clean up this event listener.
SSE Callback Event Payload:-
- Event payload that will come from
SSECallBackEvent
will look like this:-
{
"payload": {
"data": {
"name": "Mini Truck 3",
"owner": "tenants:cfecb885-e060-514a-b4f7-0094cf6f48e2",
"properties": {
"availability": true,
"fuelType": "CNG",
"mode": "truck",
"operatorId": "op-2342"
},
"uniqueCode": "2b8d45f6-7c6c-11ee-b962-0242ac120002"
},
"eventId": "ebd31aad-7712-441e-b376-b59cf1e23d06",
"X-COREOS-REQUEST-ID": "f52d1676-002a-4968-abcc-c3a651537670",
"brokerTimestamp": 1699251799599,
"agentTimestamp": 1699251799605,
"consoleTimestamp": 1699251800245
}
}
- Here, in the above mentioned example. Object will come under the detail property of the event.
- The object contains a payload property that contains
data, consoleTimestamp, agentTimeStamp, BrokerTimeStamp, eventId and X-COREOS-REQUEST-ID
. - The data property contains the response that has been received by callback url.
- The object contains a payload property that contains
BroadCasting event to users
This guide details the integration of Server-Side Events (SSE) using the OS1HttpClient class provided by the Console, allowing for real-time communication from server to client in your application. There are two ways to implement Server Side Events (SSE) broadcasting feature in your application:
Prerequisites before using broadcasting feature:-
- To use broadcasting features, developers must set the interTabCommunicationRequired flag to true for their app. They need to subscribe to topic(s) they want to listen to the broadcast messages.The maximum number of topics that can be broadcast at a time is five. There should be no commas in topic names.
Subscribing a Topic:
- Developers can subscribe to topics by calling the
subscribeBroadcastTopic
function and passing the desired topic name. It is essential to note that each app has a limit of 10 topics and a maximum of 10 subscribers per topic. To make the most out of these limits, topics should be specific and tailored to the events they represent. This would enable targeted events within the limits while ensuring precision. Therefore, specific topic names that reflect granular contexts should stay within the subscriber limits.
await cl.subscribeBroadCastTopic([ 'dispatch:e3956ddf-e7a2-4a9e-914a-ddace6a9d701', 'facility:326e7698-c039-416a-a697-768e9bbb451c:dispatches', ]);
- Developers can subscribe to topics by calling the
Using the {{SSE_BroadCast}} Placeholder
This is the simplest option if using the Console’s Axios client.
Step 1: Setting the Broadcast URL Parameter When making an API call, specify the attribute where the broadcast url is expected on the backend and its value needs to be set by using the placeholder
{{SSE_BROADCAST(topicNames)}}
. The Console will automatically detect and replace this placeholder with the actual broadcast URL. Developers can pass the callback attribute in the request URL, headers, or the request body. The example below adds{{SSE_BROADCAST(topicNames)}}
to the header for the createVehicles request.try { await axiosClient.post( '/vehicles', dto, // The payload for the API call 'createVehicles', // The operation or function being called { withAuth: false }, // Options, set to false if authentication is not required { headers: { 'Callback': '{{SSE_BROADCAST(dispatch:e3956ddf-e7a2-4a9e-914a-ddace6a9d701, facility:326e7698-c039-416a-a697-768e9bbb451c:dispatches)}}', // Updated automatically by the Axios Client 'Content-Type': 'application/json', // Set the content type of the request }, } ); } catch (error) { console.error('error', error); }
Calling the broadCastEvents
- When you are ready to make an API call, ensure that the brodacastEvents function is invoked to broadcast events. This function will accept the topicNames on which it needs to broadcast and payload which needs to be passed. See the example code below:
const broadcast = await cl.broadCastEvents(
['dispatch:e3956ddf-e7a2-4a9e-914a-ddace6a9d701'],
payload
);
Broadcast Event payload
{
"data": {
"businessCommand": "participantsboatsEntityCreate",
"callbackMeta": {},
"commandName": "ParticipantCreationSuccessEvent",
"data": {
"appId": "participants",
"createdAt": "1707463018469",
"createdBy": {
"id": "1",
"name": "vehicle-consoleUi"
},
"entityName": "boats",
"isActive": true,
"name": "sjdsadghafja",
"owner": "tenants:436a9b2c-5f79-5b51-92ba-2da25050dd36",
"properties": {
"availability": true,
"failurereason": "",
"fuelType": "CNG",
"inprogress": false,
"isfail": false,
"mode": "sdhavdhha",
"operatorId": "hdad",
"processingstage": "isActive",
"servicecode": "",
"system": false
},
"state": {
"current": "onboarding:onboarding",
"transitions": [
"active:active"
]
},
"uniqueCode": "bjhgfajdf",
"updatedAt": "1707463018469",
"updatedBy": {
"id": "1",
"name": "vehicle-consoleUi"
},
"id": "boats:c6cf6d0c-7024-5925-90d9-a803b59df46f"
},
"requestId": "d133e887-e52b-47dc-80ca-9f4228708cab",
"tenantId": "developsln1"
},
"eventId": "e5462690-e8b1-49e6-a082-82bad87ee70d",
"X-COREOS-REQUEST-ID": "",
"brokerTimestamp": 1707463018742,
"agentTimestamp": 1707463018747,
"topics": [
"TenantId:app:0641372d-802aaeb4:Test17"
],
"type": "broadcast", // by this we are distinguishing between broadcast and callback
"consoleTimestamp": 1707463018796
}
Configuration for Injectable Controls offered by the Library
Common parameters, that will be passed in all injectable controls:-
- type:- This field shows the type of injectable controls that is being used. Available types are TextBox, TextBoxButton, AutoComplete, DropDown, SearchBox
- width:- This field shows the maximum percentage of width that can be passed.
- placeholder:- This field displays the placeholder text, that is passed in injectable control.
- id:- This is unique id that is given to injectable controls that uniquely identifies that particular component.
- float:- This is optional component, by default every component is left floated.
TextBox:-
- To use textbox in console ui, we provide following configuration:-
{
type: "TextBox",
width: 100,
placeholder: "Search Package",
float: "left",
id: "TextBox1"
attributes: {
maxlength: “50”;
}
}
- Note:-
- Here, attributes is an optional attributes parameter that contains object which defines if any attribute needs to be set to injectable controls.
- OnChange and OnBlur event is emitted by this injectable control.
TextBoxButton:-
- To use textbox with button in console ui, we provide following configuration:-
{
type: "TextBoxButton",
width: 100,
placeholder: "Search Package",
float: "left",
id: "TextBoxButton1"
attributes: {
maxlength: “50”;
}
button: true,
buttonText: “Search”,
buttonColor: “red”
}
- Note:-
- Here button by default set to false, but if we set it to true, then we have to pass buttonText and buttonColor, which specifies the outlook of a button
- Here, attributes is an optional attributes parameter that contains object which defines if any attribute needs to be set to injectable controls.
- OnChange is emitted when we change text in input box and OnClick is emitted when user clicks on button.
Search Box:-
- To use SearchBox with Lens Icon in console ui, we provide following configuration:-
{
type: "SearchBox",
width: 100,
placeholder: "Search Package",
float: "right",
id: "SearchBox1"
attributes: { maxlength: “50”; }
lensIcon: true
}
- Note:-
- Here lensIcon is optional, if it is set to true then lensIcon will be shown in injectable control.
- Here, attributes is an optional attributes parameter that contains object which defines if any attribute needs to be set to injectable controls.
- OnChange, onBlur, onFocus is emitted on input box.
AutoComplete:-
- To use AutoComplete in console ui, we provide following configuration:-
{
type: "AutoComplete",
width: 100,
placeholder: "Search Package",
float: "right",
id: "AutcoComplete1"
functionBoundOption: autoComplete(), // This field can be a function which return array of Objects or normal array of objects in the form of [{ value: string, text: string }],
}
Note:-
- In this
functionBoundOption
is passed when options are static and they can be passed in the form of [{ value: string, text: string }], User can declare this as value of this field or can pass the function that returns the value in above mentioned format. - For setting options dynamically, user can set
hasAsyncFunctionBoundOption
, then they can callfunctionBoundAutoCompleteOption
from@os1-platform/console-ui-react
and pass array of objects and id of autocomplete as parameters to that function.
import { functionBoundAutoCompleteOption } from '@os1-platform/console-ui-react'; if (consoleInstance) { functionBoundAutoCompleteOption(autoCompleteValues, 'AutoComplete1'); // here firstParament is a variable that contains the array of objects that needs to be present in dropdown. Second parameter contains the id of dropdown on which we want to load the values }
- OnChange, onBlur, OnClick, OnScroll is emitted on this injectable controls
- In this
DropDown:-
- To use DropDown in console ui , we provide following configuration:-
{
type: "DropDown", // type of injectable control
width: 100, // maximum width in percentage that can be given
placeholder: "Search Package", // placeholder text
float: "right", // aligning the injectable control in left or right direction
id: "DropDown1" // unique id that can be given to injectable control
functionBoundOption: [{ value: "1", text: "Mobiles" },{ value:"2", text: "Laptops" }], // This field can be a function which return array of Objects or normal array of objects in the form of [{ value: string, text: string }],
throttleTime: 300 // this is an optional parameter which enables to set throttletime for on scroll Event. By default it is 500 ms.
}
Note:-
- In this
functionBoundOption
is passed when options are static and they can be passed in the form of [{ value: string, text: string }], User can declare this as value of this field or can pass the function that returns the value in above mentioned format. - For setting options dynamically, user can set
hasAsyncFunctionBoundOption
, then they can callfunctionBoundOption
from@os1-platform/console-ui-react
and pass array of objects and id of dropdown as parameters to that function. - For setting initial Value to dropdown,
OS1DropDownDefaultValue
from@os1-platform/console-ui-react
needs to be passed with value and id of dropdown. It can only be passed when instance of console UI is available or console UI has been loaded.
import { functionBoundOption, OS1DropDownDefaultValue, } from '@os1-platform/console-ui-react'; if (consoleInstance) { functionBoundOption(dropDownValues, 'Dropdown1'); // here firstParament is a variable that contains the array of objects that needs to be present in dropdown. Second parameter contains the id of dropdown on which we want to load the values OS1DropDownDefaultValue('initialValue', 'DropDown1'); // Here first parameter contains the value that needs to passed as initialValue, second Parameter is the id of the dropdown on which Id, needs to be set. }
- OnChange, onScroll, OnClick, OnSearch, OnBlur is emitted on this injectable controls
- In this
Configuration required in Next.js projects.
- In Next.js, we need to have some library that supports global CSS.
- One of these library is
next-global-css
, we require to importwithGlobalCss
from it and wrap our configuration within it. Link to configure next-global-css
FAQ
Which option should I select from the popup that shows when I open an app for the first time?
- The user should choose the solution that the app belongs to when there are multiple options. The user will be redirected to selected(current) solution's first app(as per display order), if the browser's URL differs from the one current selected solution.
Why header and sidebar is not visible?
- This occurs when developer has not provided appId in his code, as it is mandatory.
Why in sidebar apps are not visible?
- Verify the subscription via the console api response. If the api displays a status of 200, examine the response. The app won't be accessible if its display order is equal to or less than 0.