Added container and sample.json

This commit is contained in:
Hugo Bernier 2022-11-07 23:12:59 -05:00
parent 7850700800
commit 2d8b12f1fe
7 changed files with 152 additions and 67 deletions

View File

@ -0,0 +1,39 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.15.2",
"image": "docker.io/m365pnp/spfx:1.15.2",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

@ -1,4 +1,5 @@
# SP Site ER Diagram # SP Site ER Diagram
## Summary ## Summary
This web part loads all lists on a site and display it in a Entity Relationship Diagram (ERD) using [GoJS](https://www.npmjs.com/package/gojs). This web part loads all lists on a site and display it in a Entity Relationship Diagram (ERD) using [GoJS](https://www.npmjs.com/package/gojs).
@ -7,6 +8,7 @@ This web part loads all lists on a site and display it in a Entity Relationship
![ER Webpart in fullscreen](assets/SPERasAppPageFullScreen.png) ![ER Webpart in fullscreen](assets/SPERasAppPageFullScreen.png)
## Compatibility ## Compatibility
![SPFx 1.14](https://img.shields.io/badge/SPFx-1.14-green.svg) ![SPFx 1.14](https://img.shields.io/badge/SPFx-1.14-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg) ![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg) ![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
@ -23,15 +25,7 @@ This web part loads all lists on a site and display it in a Entity Relationship
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) > Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Solution ## Solution
<!--
We use this section to recognize and promote your contributions. Please provide one author per line -- even if you worked together on it.
We'll only use the info you provided here. Make sure to include your full name, not just your GitHub username.
Provide a link to your GitHub profile to help others find more cool things you have done.
If you provide a link to your Twitter profile, we'll promote your contribution on social media.
-->
Solution|Author(s) Solution|Author(s)
--------|--------- --------|---------
@ -44,6 +38,7 @@ Version|Date|Comments
1.0|October 07, 2022|Initial release 1.0|October 07, 2022|Initial release
## Minimal path to awesome ## Minimal path to awesome
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-pnpjs-spsite-er-diagram) then unzip it) * Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-pnpjs-spsite-er-diagram) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`react-pnpjs-spsite-er-diagram`, located under `samples`) * From your command line, change your current directory to the directory containing this sample (`react-pnpjs-spsite-er-diagram`, located under `samples`)
* in the command line run: * in the command line run:
@ -54,7 +49,7 @@ Version|Date|Comments
## Features ## Features
This project can be used as a starting point for any visualisation of SharePoint Data. Currently it's using GoJS as dependency for the ER Diagram (in productive enviroment you would need to get a license for it). The data layer is abstract so it's possible to use a different library (like three.js) as presentation layer. This project can be used as a starting point for any visualization of SharePoint Data. Currently it's using GoJS as dependency for the ER Diagram (in productive environment you would need to get a license for it). The data layer is abstract so it's possible to use a different library (like three.js) as presentation layer.
* the Data gets cached, to see changes made to the lists/lookups a "refresh" is needed * the Data gets cached, to see changes made to the lists/lookups a "refresh" is needed
* alerts/warnings for the lists are displayed as well (Versioning/ItemCount/Threshholdlimit/..) * alerts/warnings for the lists are displayed as well (Versioning/ItemCount/Threshholdlimit/..)
@ -62,41 +57,9 @@ This project can be used as a starting point for any visualisation of SharePoint
* easy switch between Internal-/DisplayName * easy switch between Internal-/DisplayName
* Download current canvas as image * Download current canvas as image
<!--
Note that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions in advance! You rock ❤.
-->
<!--
RESERVED FOR REPO MAINTAINERS
We'll add the video from the community call recording here
## Video
[![YouTube video title](./assets/video-thumbnail.jpg)](https://www.youtube.com/watch?v=XXXXX "YouTube video title")
-->
## Help ## Help
<!--
You can just search and replace this page with the following values:
Search for:
react-pnpjs-spsite-er-diagram
Replace with your sample folder name. E.g.: react-my-cool-sample
Search for:
@YOURGITHUBUSERNAME
Replace with your GitHub username, prefixed with an "@". If you have more than one author, use %20 to separate them, making sure to prefix everyone's username individually with an "@".
Example:
@hugoabernier
Or:
@hugoabernier%20@VesaJuvonen%20@PopWarner
-->
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues. We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-pnpjs-spsite-er-diagram",
"source": "pnp",
"title": "SP Site ER Diagram",
"shortDescription": "This web part loads all lists on a site and display it in a Entity Relationship Diagram (ERD)",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-pnpjs-spsite-er-diagram",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-pnpjs-spsite-er-diagram",
"longDescription": [
"This web part loads all lists on a site and display it in a Entity Relationship Diagram (ERD)"
],
"creationDateTime": "2022-11-07",
"updateDateTime": "2022-11-07",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.14"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-pnpjs-spsite-er-diagram/assets/YOUR-IMAGE-NAME-HERE",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "ICTNiklasWilhelm",
"pictureUrl": "https://github.com/ICTNiklasWilhelm.png",
"name": "Niklas Wilhelm"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -23,16 +23,16 @@ const SpSiteErDiagram: React.FC<ISpSiteDiagramProps> = (props: ISpSiteDiagramPro
const [alertsActive, setAlertsActive] = React.useState(true); const [alertsActive, setAlertsActive] = React.useState(true);
const [fieldsActive, setFieldsActive] = React.useState(true); const [fieldsActive, setFieldsActive] = React.useState(true);
const loadDiagram = async (refresh: boolean) => { const loadDiagram = async (refresh: boolean):Promise<void> => {
if(refresh) { setLoadingProgress(0); setNodeDataArray([]); } if(refresh) { setLoadingProgress(0); setNodeDataArray([]); }
// Get SP SiteData for ER Diagram // Get SP SiteData for ER Diagram
let spSiteData = await getSPSiteData(props.context, refresh, (progress) => {setLoadingProgress(progress);}); const spSiteData = await getSPSiteData(props.context, refresh, (progress) => {setLoadingProgress(progress);});
console.log("SPSiteData", spSiteData); console.log("SPSiteData", spSiteData);
// Transform to GoJS Model // Transform to GoJS Model
let goJSNodes = getGoJSNodesFromSPSiteData(spSiteData, useInternalName ? "name" : "displayName", alertsActive, fieldsActive); const goJSNodes = getGoJSNodesFromSPSiteData(spSiteData, useInternalName ? "name" : "displayName", alertsActive, fieldsActive);
// Set State // Set State
setNodeDataArray(goJSNodes.nodeDataArray.filter((n) => setNodeDataArray(goJSNodes.nodeDataArray.filter((n) =>
optionRelationOnly && goJSNodes.linkDataArray.some(l => l.from == n.key || l.to == n.key) || !optionRelationOnly // Filter optionRelationOnly optionRelationOnly && goJSNodes.linkDataArray.some(l => l.from === n.key || l.to === n.key) || !optionRelationOnly // Filter optionRelationOnly
)); ));
setLinkDataArray(goJSNodes.linkDataArray); setLinkDataArray(goJSNodes.linkDataArray);
} }
@ -42,11 +42,11 @@ const SpSiteErDiagram: React.FC<ISpSiteDiagramProps> = (props: ISpSiteDiagramPro
loadDiagram(false); loadDiagram(false);
}, [optionRelationOnly, useInternalName, alertsActive, fieldsActive]); }, [optionRelationOnly, useInternalName, alertsActive, fieldsActive]);
const downloadAsImage = () => { const downloadAsImage = ():void => {
if(diagramRef && diagramRef.current) { if(diagramRef && diagramRef.current) {
let canvas = (diagramRef.current as any).divRef.current.firstChild; const canvas = (diagramRef.current as any).divRef.current.firstChild;
console.log((diagramRef.current as any).divRef.current); console.log((diagramRef.current as any).divRef.current);
var link = document.createElement('a'); const link = document.createElement('a');
link.download = props.context.pageContext.web.title + '_ERDiagram.png'; link.download = props.context.pageContext.web.title + '_ERDiagram.png';
link.href = canvas.toDataURL() link.href = canvas.toDataURL()
link.click(); link.click();
@ -64,7 +64,7 @@ const SpSiteErDiagram: React.FC<ISpSiteDiagramProps> = (props: ISpSiteDiagramPro
{key: '4', text: "Download as image", iconProps: { iconName: 'Share' }, onClick: () => { downloadAsImage() }} {key: '4', text: "Download as image", iconProps: { iconName: 'Share' }, onClick: () => { downloadAsImage() }}
]} /> ]} />
<div className={styles.spSiteErDiagram} style={{height: "calc(100% - 44px)", padding: "0px"}}> <div className={styles.spSiteErDiagram} style={{height: "calc(100% - 44px)", padding: "0px"}}>
{ loadingProgress != 100 && nodeDataArray.length == 0 ? { loadingProgress !== 100 && nodeDataArray.length === 0 ?
<div style={{ padding: "8%" }}> <div style={{ padding: "8%" }}>
<ProgressIndicator label={`Loading Lists and Columns ${loadingProgress.toFixed(0)}%`} percentComplete={loadingProgress/100} /> <ProgressIndicator label={`Loading Lists and Columns ${loadingProgress.toFixed(0)}%`} percentComplete={loadingProgress/100} />
</div> : </div> :

View File

@ -44,19 +44,19 @@ const getSPSiteData = async (spfxContext: any, force?: boolean, progress?: (numb
} }
// Load from site // Load from site
let spSiteData: SPSiteData = { const spSiteData: SPSiteData = {
relations: [], relations: [],
tables: [] tables: []
} }
let tmp_listNames: any = {}; const tmp_listNames: any = {};
const sp = spfi().using(SPFx(spfxContext)); const sp = spfi().using(SPFx(spfxContext));
let lists = await sp.web.lists.filter("Hidden ne 1")(); const lists = await sp.web.lists.filter("Hidden ne 1")();
const totalCount = lists.filter(l => !l.Hidden).length; const totalCount = lists.filter(l => !l.Hidden).length;
let loadedCount = 0; let loadedCount = 0;
for(let list of lists) { for(const list of lists) {
if(!list.Hidden) { if(!list.Hidden) {
loadedCount++; loadedCount++;
progress && progress(loadedCount/totalCount * 100); progress && progress(loadedCount/totalCount * 100);
@ -65,11 +65,11 @@ const getSPSiteData = async (spfxContext: any, force?: boolean, progress?: (numb
tmp_listNames[`{${list.Id.toLocaleLowerCase()}}`] = list.Title; tmp_listNames[`{${list.Id.toLocaleLowerCase()}}`] = list.Title;
// Tables/Lists // Tables/Lists
let table: SPTable = { id: list.Id, title: list.Title, fields: [], alerts: [] }; const table: SPTable = { id: list.Id, title: list.Title, fields: [], alerts: [] };
// Fields // Fields
let fields = (await sp.web.lists.getById(list.Id).fields.filter("Hidden ne 1")()) const fields = (await sp.web.lists.getById(list.Id).fields.filter("Hidden ne 1")())
.filter(f => !f.Hidden && (f as any).LookupList != "AppPrincipals" && .filter(f => !f.Hidden && (f as any).LookupList !== "AppPrincipals" &&
((f as any).CanBeDeleted || (f as any).InternalName == "Title" || (f as any).InternalName == "Id") ((f as any).CanBeDeleted || (f as any).InternalName === "Title" || (f as any).InternalName === "Id")
) )
//.sort((a,b) => a.InternalName.charCodeAt(0) - b.InternalName.charCodeAt(0) ); //.sort((a,b) => a.InternalName.charCodeAt(0) - b.InternalName.charCodeAt(0) );
table.fields = fields.map(f => { table.fields = fields.map(f => {
@ -77,7 +77,7 @@ const getSPSiteData = async (spfxContext: any, force?: boolean, progress?: (numb
return { return {
name: f.InternalName, name: f.InternalName,
displayName: f.Title, displayName: f.Title,
iskey: (f as any).TypeDisplayName == "Lookup" && (f as any).IsRelationship && (f as any).LookupList != '' && (f as any).LookupList != "AppPrincipals", iskey: (f as any).TypeDisplayName === "Lookup" && (f as any).IsRelationship && (f as any).LookupList !== '' && (f as any).LookupList !== "AppPrincipals",
isunique: f.EnforceUniqueValues, isunique: f.EnforceUniqueValues,
type: f.TypeDisplayName type: f.TypeDisplayName
} }
@ -97,9 +97,9 @@ const getSPSiteData = async (spfxContext: any, force?: boolean, progress?: (numb
spSiteData.tables.push(table); spSiteData.tables.push(table);
// Links/Lookups // Links/Lookups
let relations: SPRelation[] = fields.filter(f => f.TypeDisplayName == "Lookup" && const relations: SPRelation[] = fields.filter(f => f.TypeDisplayName === "Lookup" &&
(f as any).IsRelationship && (f as any).IsRelationship &&
(f as any).LookupList != '' && (f as any).LookupList != "AppPrincipals" (f as any).LookupList !== '' && (f as any).LookupList !== "AppPrincipals"
).map<SPRelation>(f => ).map<SPRelation>(f =>
{ {
return { return {

View File

@ -30,26 +30,26 @@ const configByFieldType: any = {
"Hyperlink or Picture": { color: colors.blue, figure: "Circle" } "Hyperlink or Picture": { color: colors.blue, figure: "Circle" }
} }
const getNodeItemFromField = (f: SPTableField, fieldNameProperty: string = "name") : GoJSNodeItem => { const getNodeItemFromField = (f: SPTableField, fieldNameProperty: string = "name") : GoJSNodeItem => {
let c = configByFieldType[f.type] || configByFieldType['default']; const c = configByFieldType[f.type] || configByFieldType['default'];
let prefix = f.type == "Counter" ? "PK | " : (f.iskey && f.type == "Lookup" ? "FK | " : ""); const prefix = f.type === "Counter" ? "PK | " : (f.iskey && f.type === "Lookup" ? "FK | " : "");
return { return {
name: prefix + (f as any)[fieldNameProperty] + ` (${f.type})`, name: prefix + (f as any)[fieldNameProperty] + ` (${f.type})`,
iskey: f.iskey, iskey: f.iskey,
figure: c.figure, figure: c.figure,
color: f.iskey ? colors.keycolor : c.color, color: f.iskey ? colors.keycolor : c.color,
order: f.type == "Counter" ? "1" : order: f.type === "Counter" ? "1" :
f.type == "Lookup" && f.iskey ? "2" : f.type === "Lookup" && f.iskey ? "2" :
f.type == "Lookup" ? "3" : f.type === "Lookup" ? "3" :
f.type f.type
}; };
} }
const configByAlert: any = { const configByAlert = {
'Info': { color: colors.lightblue, figure: "Rectangle" }, 'Info': { color: colors.lightblue, figure: "Rectangle" },
'Warning': { color: colors.orange, figure: "Rectangle" }, 'Warning': { color: colors.orange, figure: "Rectangle" },
'Error': { color: colors.red, figure: "Rectangle" }, 'Error': { color: colors.red, figure: "Rectangle" },
} }
const getNodeItemFromAlert = (a: SPTableAlert) : GoJSNodeItem => { const getNodeItemFromAlert = (a: SPTableAlert) : GoJSNodeItem => {
let c = configByAlert[a.type]; const c = configByAlert[a.type];
return { return {
name: "#" + a.type + " | " + a.title, name: "#" + a.type + " | " + a.title,
iskey: false, iskey: false,