How to refresh powerbi-client-react component once token has expired? - reactjs

I am testing a react web app where I can display reports from Power BI. I am using powerbi-client-react to embed the reports. However, I face an issue when I load the component with an expired token, I get this error: Content not available screenshot.
So whenever that happens, I catch it with the error event handler, get a new token and update the powerbi report accessToken. However, it doesn't seem to reload/refresh the embed when I set the new accessToken in react. It only displays the screenshot above.
Error log screenshot.
Is there a way to force refresh the embed component with the new access token? or is my approach not correct? Any mistakes pointer would be appreciated.
import React from 'react';
import {models} from 'powerbi-client';
import {PowerBIEmbed} from 'powerbi-client-react';
// Bootstrap config
let embedConfigTest = {
type: 'report', // Supported types: report, dashboard, tile, visual and qna
id: reportId,
embedUrl: powerBIEmbedURL,
accessToken: null,
tokenType: models.TokenType.Embed,
pageView: 'fitToWidth',
settings: {
panes: {
filters: {
expanded: false,
visible: false,
},
},
background: models.BackgroundType.Transparent,
},
};
const PowerBiReport = ({graphName, ...props}) => {
let [embedToken, setEmbedToken] = React.useState();
let [embedConfig, setEmbedConfig] = React.useState(embedConfigTest);
React.useEffect(
() => {
setEmbedToken(EXPIRED_TOKEN);
setEmbedConfig({
...embedConfig,
accessToken: EXPIRED_TOKEN, // Initiate with known expired token
});
},
[graphName]
);
const changeSettings = (newToken) => {
setEmbedConfig({
...embedConfig,
accessToken: newToken,
});
};
// Map of event handlers to be applied to the embedding report
const eventHandlersMap = new Map([
[
'loaded',
function() {
console.log('Report has loaded');
},
],
[
'rendered',
function() {
console.log('Report has rendered');
},
],
[
'error',
async function(event, embed) {
if (event) {
console.error(event.detail);
console.log(embed);
// Simulate getting a new token and update
setEmbedToken(NEW_TOKEN);
changeSettings(NEW_TOKEN);
}
},
],
]);
return (
<PowerBIEmbed
embedConfig={embedConfig}
eventHandlers={eventHandlersMap}
cssClassName={'report-style-class'}
/>
);
};
export default PowerBiReport;

Thanks #vtCode. Here is a sample but the refresh can only happen in 15 secs interval.
import { PowerBIEmbed } from "powerbi-client-react";
export default function PowerBiContainer({ embeddedToken }) {
const [report, setReport] = useState(null);
useEffect(() => {
if (report == null) return;
report.refresh();
}, [report, embeddedToken]);
return (
<PowerBIEmbed
embedConfig={{ ...embedConfig, accessToken: embeddedToken }}
getEmbeddedComponent={(embeddedReport) => setReport(embeddedReport)};
/>
);
}
Alternatively, you can add the React "key" attribute which remounts the component when embededToken changes
<PowerBIEmbed key={embeddedToken}
embedConfig={{ ...embedConfig, accessToken: embeddedToken }}
/>

I ended up solving this issue, although not so beautiful.
I checked the powerbi-client wiki as it has dependency on it and found out that you could use embed.reload() in the embed object I get from the error function.
For some reason (I could not find out why), the error handler gets triggered twice, so to avoid refreshing the token twice, I had to create a dialog notifying the user that the token had expired and whenever that dialog is closed, I reload the powerbi report.
Exact wiki reference:
Overriding Error Experience
Reload a report
Update embed token

Related

Agora screen sharing in electron with react, Gives permission denied

I am building an electron-react app with Agora for calls. I am trying to implement the screen sharing feature.
As described in the following documentation I tried requesting screen sharing.
https://docs.agora.io/en/video/screensharing_web_ng?platform=Web#screen-sharing-on-electron
But I am getting the following error
AgoraRTCError PERMISSION_DENIED: NotAllowedError: Permission denied
I tried invoking the same function AgoraRTC.createScreenVideoTrack in react and electron but the result was the same.
How do I get permission to share the screen in electron with agora?
After some research, I found the electron way of doing it.
First I request to share the screen.
const turnScreenSharingOn = async () => {
const sources = await window.electron.media.getDesktopSources();
setScreenSources(sources);
setScreenSharePopupVisible(true);
};
window.electron.media.getDesktopSources() is a electron preload function that fetches the screen sources.
getDesktopSources: async () =>
await desktopCapturer
.getSources({
types: ['window', 'screen'],
})
.then((sources) =>
sources.map((source) => ({
id: source.id,
name: source.name,
appIconUrl: source?.appIcon?.toDataURL(),
thumbnailUrl: source?.thumbnail
?.resize({ height: 160 })
.toDataURL(),
}))
)
Once I have the sources I show the popup from which I choose the source and pass the source id to the next function.
const onScreenChoose = async (sourceId: string, cb?: () => void) => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
},
} as MediaTrackConstraints,
});
const track = stream.getVideoTracks()[0];
const screenTrack = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: track,
});
window.agora.screenTrack?.setEnabled(false);
window.agora.screenTrack = screenTrack;
setScreenTrack(screenTrack);
await screenShareClient.publish(screenTrack);
setScreenSharePopupVisible(false);
cb?.();
};

Smart PayPal Buttons in React, disable the button

I have this implementation of the PayPal smart buttons in React:
function PayPalButtonComponent(props: PayPalButtonProps) {
const [show, set_show] = useState(false);
const [error, set_error] = useState<string>();
const create_order = (_: any, actions: any) => {
return actions.order.create({
purchase_units: [
{
amount: {
currency: props.currency || "EUR",
value: props.total
}
}
]
});
};
const handle_approve = (_: any, actions: any) => {
return actions.order.capture().then((details: any) => {
if (props.onSuccess) props.onSuccess(details);
});
};
const handle_cancel = () => {
if (props.onCancel) props.onCancel();
};
const handle_error = () => {
if (props.onError) props.onError();
};
const render_button = () => {
const Button = paypal.Buttons.driver("react", { React, ReactDOM });
return (
<Button
style={{
layout: "horizontal",
size: "responsive",
shape: "rect",
color: "gold",
tagline: false
}}
funding={{
allowed: [paypal.FUNDING.CARD, paypal.FUNDING.PAYPAL],
disallowed: [paypal.FUNDING.CREDIT]
}}
createOrder={create_order}
onApprove={handle_approve}
onError={handle_error}
onCancel={handle_cancel}
/>
);
};
useEffect(() => {
if (props.isScriptLoaded) {
if (props.isScriptLoadSucceed) set_show(true);
else set_error("Unable to load the paypalscript");
}
}, [props.isScriptLoaded, props.isScriptLoadSucceed]);
if (error) return <p>{error}</p>;
if (!show) return <FakeButton />;
return render_button();
}
I have struggled to implement these buttons in react since there is no documentation, I have found and copied some code from here and trying to guess other stuff. But I can't understand how to disable the button.
In this guide they state that one can call the disable() method on the actions object but can't figure out how I can accomplish that with my configuration.
Have you ever try something similar? Do you know any documentation one can follow?
edit
What I'm trying to accomplish is to set the button in a disable state during the payment. I know there is the paypal overlay, but when the transaction completes I change the app route and since it happen when onSuccess is called, due to the apparent async nature of actions.order.capture() this can't happen instantaneously, and so there is a moment when one can click again on the paypal button. If i can disable the button i have solve the problem.
The onInit implementation allows you to disable/enable the button before clicking on it, useful for some sort of validation before the checkout (like terms checked) but doesn't apply to my case. I have also tried to call actions.disable() in create_order but that breaks the button.
Have same situation here and the onInit example from documentation is not that straight forward as some think... You would need to create hidden imput and assign somsort of variable to it when payment made in order to achieve buttons to be disabled after payment. To bad no one solve this one.

Why is first Jest test causing second test to fail?

I have a React component which renders a list of components. I'm running some tests which mock the axios module which loads in the users from JSONPlaceHolder. All works fine including the the async test and it's mocks data as expected. However if you look at the code below it only passes as long as the first test is commented out? Am I missing something? Been banging my head for ages. Is there some cleanup that needs to be done between tests? Thanks in advance.
import { waitForElement } from 'enzyme-async-helpers';
import UsersList from '../UsersList';
import axios from 'axios';
const mockUsers = [
{
"id": 1,
"name": "Leanne Mock",
"username": "Bret",
"email": "Sincere#april.biz"
},
{
"id": 2,
"name": "John Mock",
"username": "Jospeh",
"email": "wacky#april.biz"
}
]
axios.get.mockImplementationOnce(() => Promise.resolve({
data: mockUsers
}))
describe('<UsersList /> tests:', () => {
//WHEN I UNCOMMENT THIS TEST THE SECOND TEST FAILS?
test('It renders without crashing', (done) => {
// const wrapper = shallow(<UsersList />);
});
test('It renders out <User /> components after axios fetches users', async () => {
const wrapper = shallow(<UsersList />);
expect(wrapper.find('#loading').length).toBe(1); //loading div should be present
await waitForElement(wrapper, 'User'); //When we have a User component found we know data has loaded
expect(wrapper.find('#loading').length).toBe(0); //loading div should no longer be rendered
expect(axios.get).toHaveBeenCalledTimes(1);
expect(wrapper.state('users')).toEqual(mockUsers); //check the state is now equal to the mockUsers
expect(wrapper.find('User').get(0).props.name).toBe(mockUsers[0].name); //check correct data is being sent down to User components
expect(wrapper.find('User').get(1).props.name).toBe(mockUsers[1].name);
})
})
The Error message I get is:
The render tree at the time of timeout:
<div
id="loading"
>
Loading users
</div>
console.warn node_modules/enzyme-async-helpers/lib/wait.js:42
As JSON:
{ node:
{ nodeType: 'host',
type: 'div',
props: { id: 'loading', children: ' Loading users ' },
key: undefined,
ref: null,
instance: null,
rendered: ' Loading users ' },
type: 'div',
props: { id: 'loading' },
children: [ ' Loading users ' ],
'$$typeof': Symbol(react.test.json) }
Test Suites: 1 failed, 1 total
Tests: 2 failed, 2 total
You only mock the first axios.get call because you are using mockImplementationOnce.
When you shallow(<UsersList />) twice, the second time is timing out loading the users.
You can add a beforeEach method with a mockResolvedValueOnce inside, to mock the axios.get before every single test:
beforeEach(() => {
axios.get.mockResolvedValueOnce({data: mockUsers});
}
Having the same issue, but I'm not making a request. I'm building a client-side React application and testing for the render of sub-components. I have an image carousel that loads on my Home component and I'm writing tests for it. If I comment out all but one test (any test) it passes. If I have more than one test (any combination of tests), it fails. I've tried async/await/waitFor, react-test-renderer, using done() - nothing seems to change this behavior.
import { render, screen } from '#testing-library/react';
import ImageCarousel from '../carousel/ImageCarousel';
import localPhotos from '../../data/localPhotos';
// passing in the full array of images is not necessary, it will cause the test to time out
const testArray = localPhotos.slice(0, 3);
describe('Image Carousel', () => {
it('renders without error', () => {
render(<ImageCarousel images={testArray} />);
const imageCarousel = screen.getByTestId('image-carousel');
expect(imageCarousel).toBeInTheDocument();
});
// it('displays the proper alt text for images', () => {
// render(<ImageCarousel images={testArray} />);
// const photo1 = screen.getByAltText(localPhotos[0].alt);
// const photo2 = screen.getByAltText(localPhotos[1].alt);
// expect(photo1.alt).toBe(localPhotos[0].alt);
// expect(photo2.alt).toBe(localPhotos[1].alt);
// });
// it("displays full-screen icons", () => {
// render(<ImageCarousel images={testArray} />);
// const fullScreenIcons = screen.getAllByTestId('full-screen-icon');
// expect(fullScreenIcons.length).toBe(testArray.length);
// })
// shows controls when showControls is true
// does not show controls when showControls is false
// it('displays the proper number of images', () => {
// render(<ImageCarousel images={testArray} />);
// const carousel_images = screen.getAllByTestId('carousel_image');
// expect(carousel_images.length).toBe(testArray.length);
// });
// calls next when clicked
// calls previous when clicked
// returns to first image when next is clicked and last image is shown
// moves to last image when previous is clicked and first image is shown
});

Electrode doesn't show dynamic data in page source

Using electrode, I noticed this weird behaviour -
When I view the page source after the page fully loads with all the api calls and data, I am only able to view the content that is static for example, the hyper links, headings, footer links etc.
I have created a custom token handler which checks the context object and populates the custom tokens present in the index.html.
So, whenever, I console.log(context.user.content), only the data that is static such as hyperlinks, headings, footer links are logged.
I guess this is the problem but I am not able to wrap my head around as to why electrode doesn't recognise the content being rendered dynamically.
Token-Handler.js file
import Helmet from 'react-helmet';
const emptyTitleRegex = /<title[^>]*><\/title>/;
module.exports = function setup(options) {
// console.log({ options });
return {
INITIALIZE: context => {
context.user.helmet = Helmet.renderStatic();
},
PAGE_TITLE: context => {
const helmet = context.user.helmet;
const helmetTitleScript = helmet.title.toString();
const helmetTitleEmpty = helmetTitleScript.match(emptyTitleRegex);
return helmetTitleEmpty ? `<title>${options.routeOptions.pageTitle}</title>` : helmetTitleScript;
},
REACT_HELMET_SCRIPTS: context => {
const scriptsFromHelmet = ["link", "style", "script", "noscript"]
.map(tagName => context.user.helmet[tagName].toString())
.join("");
return `<!--scripts from helmet-->${scriptsFromHelmet}`;
},
META_TAGS: context => {
console.log(context,'123') //this is where I am checking
return context.user.helmet.meta.toString();
}
};
};
default.js
module.exports = {
port: portFromEnv() || "3000",
webapp: {
module: "electrode-react-webapp/lib/express",
options: {
prodBundleBase: '/buy-used-car/js/',
insertTokenIds: false,
htmlFile: "./{{env.APP_SRC_DIR}}/client/index.html",
paths: {
"*": {
content: {
module: "./{{env.APP_SRC_DIR}}/server/views/index-view"
},
}
},
serverSideRendering: true,
tokenHandler: "./{{env.APP_SRC_DIR}}/server/token-handler"
}
}
};
Any clue anyone?
EDIT 1
However, any following updates that occur on the meta tags are rendered. I'm not sure that is something electrode allows or is a feature of react-helmet.
EDIT 2
SSR is enabled in electrode.
After digging in the docs, realised that there was a slight misunderstanding. So, if data needs to be present in the page source, it needs to be pre-rendered by the server.
Why it wasn't showing at the time I asked the question? Because, data was being evaluated at run-time due ot which the page source only rendered the static content.
Electrode already provides an abstraction, each component that is being rendered has an option to load with pre-fetched data. The catch here is, you have to evaluate what all data needs to be present at runtime because more data is directly proportional to page loading time (as the server won't resolve unless the api you are depending on returns you with either a success or failure )
In terms of implementation, each route has a parameter called init-top which is executed before your page loads.
const routes = [
{
path: "/",
component: withRouter(Root),
init: "./init-top",
routes: [
{
path: "/",
exact: true,
component: Home,
init: "./init-home"
},
in init-home, you can define it something on the lines of -
import reducer from "../../client/reducers";
const initNumber = async () => {
const value = await new Promise(resolve => setTimeout(() => resolve(123), 2000));
return { value };
};
export default async function initTop() {
return {
reducer,
initialState: {
checkBox: { checked: false },
number: await initNumber(),
username: { value: "" },
textarea: { value: "" },
selectedOption: { value: "0-13" }
}
};
}
So,now whenever you load the component, it is loaded with this initialState returned in init-home
I'll just post it here, in case anyone is stuck.

createContainer() Cannot retrieve object by ID

I am currently building a noughts and crosses game with Meteor and React. When I first start the app, I create a new item in the 'Games' collection and send the id of this object to the App as a prop:
Meteor.startup(() => {
Meteor.call('games.create', function(error, result) {
var id = result;
render(<App gameId={id} />, document.getElementById('render-target'));
});
});
I then use createContainer on the App component to retrieve the Game on the Database, so I can manipulate it within the App:
export default createContainer(({gameId}) => {
Meteor.subscribe('games');
console.log(gameId);
return {
gameState: Games.find(ObjectId(gameId)).fetch(),
};
}, App);
After I added this createContainer code, the app stopped rendering. In the console, I get a very long error message, copied below. Thanks in advance!
Exception in delivering result of invoking 'games.create': require<.imports.ui["App.jsx"]</exports.default<#http://localhost:3000/app/app.js?hash=d138fc2a4d2ee444a0b4b78d568bc9e3acf71f7c:758:5
getMeteorData#http://localhost:3000/packages/react-meteor-data.js?hash=913595fb1851d69206a4cd0ce5bfbacdc56a8830:282:16
calculateData/this.computation</<#http://localhost:3000/packages/react-meteor-data.js?hash=913595fb1851d69206a4cd0ce5bfbacdc56a8830:164:22
Tracker.Computation.prototype._compute#http://localhost:3000/packages/tracker.js?hash=9f8a0cec09c662aad5a5e224447b2d4e88d011ef:339:5
Tracker.Computation#http://localhost:3000/packages/tracker.js?hash=9f8a0cec09c662aad5a5e224447b2d4e88d011ef:229:5
Tracker.autorun#http://localhost:3000/packages/tracker.js?hash=9f8a0cec09c662aad5a5e224447b2d4e88d011ef:604:11
calculateData/this.computation<#http://localhost:3000/packages/react-meteor-data.js?hash=913595fb1851d69206a4cd0ce5bfbacdc56a8830:156:16
Tracker.nonreactive#http://localhost:3000/packages/tracker.js?hash=9f8a0cec09c662aad5a5e224447b2d4e88d011ef:631:12
calculateData#http://localhost:3000/packages/react-meteor-data.js?hash=913595fb1851d69206a4cd0ce5bfbacdc56a8830:155:26
componentWillMount#http://localhost:3000/packages/react-meteor-data.js?hash=913595fb1851d69206a4cd0ce5bfbacdc56a8830:66:21
require<.node_modules["react-dom"].lib["ReactCompositeComponent.js"]</ReactCompositeComponent.performInitialMount/<#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18931:18
measureLifeCyclePerf#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18658:12
require<.node_modules["react-dom"].lib["ReactCompositeComponent.js"]</ReactCompositeComponent.performInitialMount#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18930:9
require<.node_modules["react-dom"].lib["ReactCompositeComponent.js"]</ReactCompositeComponent.mountComponent#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18841:16
require<.node_modules["react-dom"].lib["ReactReconciler.js"]</ReactReconciler.mountComponent#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:11766:18
require<.node_modules["react-dom"].lib["ReactCompositeComponent.js"]</ReactCompositeComponent.performInitialMount#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18954:18
require<.node_modules["react-dom"].lib["ReactCompositeComponent.js"]</ReactCompositeComponent.mountComponent#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:18841:16
require<.node_modules["react-dom"].lib["ReactReconciler.js"]</ReactReconciler.mountComponent#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:11766:18
mountComponentIntoNode#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:23920:16
require<.node_modules["react-dom"].lib["Transaction.js"]</TransactionImpl.perform#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:12730:13
batchedMountComponentIntoNode#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:23942:3
require<.node_modules["react-dom"].lib["Transaction.js"]</TransactionImpl.perform#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:12730:13
require<.node_modules["react-dom"].lib["ReactDefaultBatchingStrategy.js"]</ReactDefaultBatchingStrategy.batchedUpdates#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:21609:14
batchedUpdates#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:11398:10
require<.node_modules["react-dom"].lib["ReactMount.js"]</ReactMount._renderNewRootComponent#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:24136:5
require<.node_modules["react-dom"].lib["ReactMount.js"]</ReactMount._renderSubtreeIntoContainer#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:24217:21
require<.node_modules["react-dom"].lib["ReactMount.js"]</ReactMount.render#http://localhost:3000/packages/modules.js?hash=b2852da4b705e3b46ab5b20c9b94e15e453af050:24238:12
require<.client["main.js"]</</<#http://localhost:3000/app/app.js?hash=d138fc2a4d2ee444a0b4b78d568bc9e3acf71f7c:395:3
Meteor.bindEnvironment/<#http://localhost:3000/packages/meteor.js?hash=e3f53db3be730057fed1a5f709ecd5fc7cae1229:1105:17
._maybeInvokeCallback#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:3557:7
.receiveResult#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:3577:5
._livedata_result#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:4742:7
Connection/onMessage#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:3385:7
._launchConnection/self.socket.onmessage/<#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:2736:11
_.forEach#http://localhost:3000/packages/underscore.js?hash=cde485f60699ff9aced3305f70189e39c665183c:149:7
._launchConnection/self.socket.onmessage#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:2735:9
REventTarget.prototype.dispatchEvent#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:175:9
SockJS.prototype._dispatchMessage#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:1160:5
SockJS.prototype._didMessage#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:1218:13
SockJS.websocket/that.ws.onmessage#http://localhost:3000/packages/ddp-client.js?hash=bc32a166cd269e06a394f9418e0024d805bab379:1365:9
Your problem is the ObjectId() meteor doesn't know how to handle it.
do the following:
export default createContainer(({gameId}) => {
Meteor.subscribe('games');
console.log(gameId);
return {
gameState: Games.find(new Meteor.Collection.ObjectID(gameId)).fetch(),
};
}, App);
Ideally you need to use the Subscription as a handler:
export default createContainer(({ gameId }) => {
const gamesHandle = Meteor.subscribe('games');
const isLoadingGames = !gamesHandle.ready()
const gamesState = Games.find({ _id: gameId }).fetch() // <-- make sure 'Games' is imported or avilable globally
return {
gamesState,
isLoadingGames
};
}, App);
Then you can access this.props.isLoadingGames in your <App /> component and use it to display a loading :) e.x this.props.isLoadingGames ? 'Loading...' : 'Finished...'

Resources