Skip to content

Commit

Permalink
UIAPPS-164 Start custom widget with 3rd-party data (#125)
Browse files Browse the repository at this point in the history
Motivation
----------

- We want to make a template for a custom widget that interacts with
    3rd-party data. This should continue to grow and change as features
    evolve.
- Jira Issue: https://datadoghq.atlassian.net/browse/UIAPPS-164

Changes
-------

- We create an example we can use as a template that solves a specific
    use case: an App where there's a custom widget that interacts with
    3rd-party data.
- We're following a lot of the setup from the custom widget with
    1st-party data from #120.

Testing
-------

- This is likely best tested once we have support for templates in the
    Datadog UI. We are almost assuredly going to need to make
    adjustments to this template afteward.

Releases
--------

Choose one:

- [x] No release is necessary.
    If you're only updating examples/documentation, this is likely what you want.
- [ ] All packages that need a release have a changeset.
  • Loading branch information
joneshf-dd authored Apr 1, 2022
1 parent 77766af commit 56ced43
Show file tree
Hide file tree
Showing 24 changed files with 586 additions and 0 deletions.
24 changes: 24 additions & 0 deletions examples/custom-widget-with-3rd-party-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.eslintcache

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
1 change: 1 addition & 0 deletions examples/custom-widget-with-3rd-party-data/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
7 changes: 7 additions & 0 deletions examples/custom-widget-with-3rd-party-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Datadog App

## Custom widget with 3rd-party data

This App provides a custom widget that interacts with 3rd-party data.
3rd-party data is data provided by something external to Datadog (like your service or another service).
It can be used as a starting point for building out a real-world App on the Datadog Developer Platform.
48 changes: 48 additions & 0 deletions examples/custom-widget-with-3rd-party-data/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "datadog-app-example-custom-widget-with-3rd-party-data",
"version": "0.0.0",
"private": true,
"dependencies": {
"@datadog/ui-extensions-react": "0.30.1",
"@datadog/ui-extensions-sdk": "0.30.1",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"@types/node": "^14.14.14",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"react-scripts": "4.0.3",
"typescript": "^4.5.4"
},
"scripts": {
"build": "react-scripts build",
"eject": "react-scripts eject",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"lint:check": "eslint .",
"lint:fix": "yarn run lint:check --fix",
"lint": "yarn run lint:check && yarn run format:check",
"prepare": "exit 0",
"start": "react-scripts start",
"test": "react-scripts test --passWithNoTests --watchAll=false"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/custom-widget /index.html 200
Binary file not shown.
36 changes: 36 additions & 0 deletions examples/custom-widget-with-3rd-party-data/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="Datadog App"
content="A simple scaffolding for Datadog Apps"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Datadog App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>
3 changes: 3 additions & 0 deletions examples/custom-widget-with-3rd-party-data/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
38 changes: 38 additions & 0 deletions examples/custom-widget-with-3rd-party-data/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DDClient, init } from '@datadog/ui-extensions-sdk';
import { getUser, User } from '../user';

/**
* We initialize the {@link DDClient} in one place and use it throughout the App.
* Having the {@link DDClient} initialized in one place helps centralize auth logic and provide better type inference.
*/
const client: DDClient = init({
authProvider: {
/**
* The `authStateCallback` can be pretty simple,
* but can do whatever you need to check the authentication status.
* This should not be used to perform actual authentication,
* only to get the current authentication status.
*
* In this case,
* we grab the current user if they exist,
* and return the appropriate `isAuthenticated` value.
*/
authStateCallback: async (): Promise<boolean> => {
const user: User | undefined = await getUser();
return user != null;
},
/**
* We use `'close'` so the login page can close the window to notify successful login.
* Once that happens, the `authStateCallback` is invoked again to check the state.
*
* @see https://github.com/DataDog/apps/blob/-/docs/en/programming-model.md#authentication
*/
resolution: 'close',
/**
* This where we want Datadog to direct users to authenticate.
*/
url: '/login'
}
});

export { client };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { client } from './client';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DDClient } from '@datadog/ui-extensions-sdk';
import * as React from 'react';
import ReactDOM from 'react-dom';
import { useSetupCustomWidget } from './custom-widget';

type ControllerProps = {
client: DDClient;
};

/**
* This component renders the main controller.
* The main controller responds to the initial handshake from Datadog and sets up any App-wide behavior.
*
* @see https://github.com/DataDog/apps/blob/-/docs/en/programming-model.md#main-controller-iframe
*/
function Controller(props: ControllerProps): JSX.Element {
useSetupCustomWidget(props.client);

return (
<>
<div>The application controller is running in the background.</div>
<a href="http://localhost:3000/custom-widget">
Click here to open your custom widget
</a>
</>
);
}

function renderController(client: DDClient) {
ReactDOM.render(
<React.StrictMode>
<Controller client={client} />
</React.StrictMode>,
document.getElementById('root')
);
}

export { renderController };
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
DDClient,
EventType,
WidgetSettingsMenuClickData
} from '@datadog/ui-extensions-sdk';
import * as React from 'react';
import { logoutUser } from '../user';

/**
* This hook performs any app-wide for the custom widget.
* @param client The initialized {@link DDClient}
*/
function useSetupCustomWidget(client: DDClient): void {
/**
* We set up an event listener for the logout widget settings menu item.
* This event handler lets us perform the actual logging out of a user.
*/
React.useEffect(() => {
const unsubscribeLogout = client.events.on(
EventType.WIDGET_SETTINGS_MENU_CLICK,
async (data: WidgetSettingsMenuClickData): Promise<void> => {
/**
* We only want to handle events from the `'logout'` settings menu item.
*/
if (data.menuItem.key !== 'logout') {
return;
}

/**
* Perform the actual logout,
* then make sure to update the auth state so it's reflected in Datadog.
*/
await logoutUser();
await client.auth.updateAuthState();
}
);

/**
* We make sure to unsubscribe the event listener we set up.
*/
return () => {
unsubscribeLogout();
};
}, [client.auth, client.events]);
}

export { useSetupCustomWidget };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderController } from './controller';
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DDClient } from '@datadog/ui-extensions-sdk';
import React from 'react';
import ReactDOM from 'react-dom';
import { getUser, User } from '../user';
import { Post } from '../post';

type CustomWidgetProps = {
client: DDClient;
};

/**
* This component brings together all the pieces and renders a custom widget.
*/
function CustomWidget(props: CustomWidgetProps): JSX.Element {
/**
* We grab the {@link User} from the 3rd-party.
*/
const [user, setUser] = React.useState<User>();
React.useEffect(() => {
getUser().then((user?: User): void => {
setUser(user);
});
}, []);

/**
* If we do not have a {@link User},
* then authentication was not successful.
* Datadog will not actually render the custom widget in this case,
* but we still have to handle this case.
*/
if (user == null) {
return <></>;
}

return (
<div
style={{
fontFamily: 'helvetica, arial, sans-serif',
margin: '2rem'
}}
>
<h2>Custom widget with 3rd-party data</h2>
<p>Posts from {user.username}!</p>
<ol>
{user.posts.map(
(post: Post): JSX.Element => {
return <li key={post.title}>{post.title}</li>;
}
)}
</ol>
</div>
);
}

function renderCustomWidget(client: DDClient) {
ReactDOM.render(
<React.StrictMode>
<CustomWidget client={client} />
</React.StrictMode>,
document.getElementById('root')
);
}

export { renderCustomWidget };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderCustomWidget } from './custom-widget';
24 changes: 24 additions & 0 deletions examples/custom-widget-with-3rd-party-data/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { client } from './client';
import { renderController } from './controller';
import { renderCustomWidget } from './custom-widget';
import { renderLogin } from './login';

switch (window.location.pathname) {
case '/custom-widget':
renderCustomWidget(client);
break;

/**
* This authentication route is part of this App,
* but the system is designed to be able to leverage a preexisting authentication route if you have access to it.
* If you already have an authentication route,
* this route should be removed.
*/
case '/login':
renderLogin(client);
break;

default:
renderController(client);
break;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderLogin } from './login';
Loading

0 comments on commit 56ced43

Please sign in to comment.